From 4ca103d7e92a203fc0a00909c5c65d3e1bc2d66b Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:17:50 +0900 Subject: [PATCH 01/97] Add mixed precision support with configurable precision zones Introduce a zone-based mixed precision system that allows each computational component (AO/MO evaluation, Jastrow, determinant, Coulomb, kinetic energy, MCMC, GFMC, optimization, I/O) to run independently in float32 or float64. The default mode (`"full"`) keeps all zones at float64 for backward compatibility, while `"mixed"` sets low-risk zones to float32 and keeps numerically sensitive zones at float64. Core changes: - New module `jqmc/_precision.py` (`get_dtype`, `configure`, `get_tolerance`) - All 15 source modules parameterized with `get_dtype("zone")` - EPS constants made dtype-aware via `get_eps()` in `_setting.py` - TOML `[precision]` section parsed in `jqmc_cli.py` - `jqmc_workflow` (VMC/MCMC/LRDMC) accepts a `precision_mode` parameter Test infrastructure: - Dtype-aware tolerances via `get_tolerance(zone, level)` - `@numerical_diff` marker: skip FD tests in mixed mode - `@external_reference` marker: skip TurboRVB tests in mixed mode - Zone-corrected tolerances for cross-zone gradient comparisons Documentation and examples: - Module docstrings with Precision Zone annotations - User guide (`doc/notes/mixed_precision.md`) - Sphinx autodoc entries for `_precision.py` and `_setting.py` - TOML `[precision]` examples in `jqmc-example01` - Workflow example (`run_pes_pipeline.py`) with `PRECISION_MODE` config --- doc/index.md | 1 + doc/notes/mixed_precision.md | 105 ++++++ examples/jqmc-example01/02vmc_JSD/vmc.toml | 14 + examples/jqmc-example01/03mcmc_JSD/mcmc.toml | 3 + .../jqmc-example01/04lrdmc_JSD/lrdmc.toml | 3 + .../run_pes_pipeline.py | 7 + jqmc/_jqmc_utility.py | 22 +- jqmc/_precision.py | 273 ++++++++++++++ jqmc/_setting.py | 60 ++- jqmc/atomic_orbital.py | 227 ++++++----- jqmc/coulomb_potential.py | 214 ++++++++--- jqmc/determinant.py | 154 ++++++-- jqmc/hamiltonians.py | 30 +- jqmc/jastrow_factor.py | 354 +++++++++++------- jqmc/jqmc_cli.py | 8 + jqmc/jqmc_gfmc.py | 163 +++++--- jqmc/jqmc_mcmc.py | 212 +++++++---- jqmc/jqmc_tool.py | 14 +- jqmc/molecular_orbital.py | 16 +- jqmc/structure.py | 67 ++-- jqmc/swct.py | 25 +- jqmc/trexio_wrapper.py | 40 +- jqmc/wavefunction.py | 109 +++--- jqmc_workflow/lrdmc_workflow.py | 14 + jqmc_workflow/mcmc_workflow.py | 14 + jqmc_workflow/vmc_workflow.py | 14 + tests/conftest.py | 52 ++- tests/test_AOs.py | 128 +++---- tests/test_MOs.py | 70 ++-- tests/test_ao_basis_optimization.py | 78 +++- tests/test_comparison_with_turborvb_AE.py | 3 + tests/test_comparison_with_turborvb_ECP.py | 3 + tests/test_determinant.py | 141 +++---- tests/test_ecps.py | 124 +++--- tests/test_hamiltonian.py | 36 +- tests/test_jastrow.py | 236 +++++------- tests/test_jqmc_gfmc_bra.py | 22 +- tests/test_jqmc_gfmc_tau.py | 24 +- tests/test_jqmc_mcmc.py | 67 ++-- tests/test_jqmc_tool.py | 30 +- tests/test_lrdmc_force.py | 20 +- tests/test_mcmc_force.py | 11 +- tests/test_structure.py | 28 +- tests/test_swct.py | 14 +- tests/test_wave_function.py | 89 ++--- 45 files changed, 2214 insertions(+), 1125 deletions(-) create mode 100644 doc/notes/mixed_precision.md create mode 100644 jqmc/_precision.py diff --git a/doc/index.md b/doc/index.md index 683f9870..bc791918 100644 --- a/doc/index.md +++ b/doc/index.md @@ -21,6 +21,7 @@ install examples notes/technical_notes notes/workflows +notes/mixed_precision api_reference_cli api_reference_cli_tool api_reference_mod/modules diff --git a/doc/notes/mixed_precision.md b/doc/notes/mixed_precision.md new file mode 100644 index 00000000..c81116f7 --- /dev/null +++ b/doc/notes/mixed_precision.md @@ -0,0 +1,105 @@ +# Mixed Precision + +jQMC supports mixed precision computation, allowing selected parts of the +calculation to run in float32 while keeping numerically sensitive operations +in float64. This can reduce memory usage by ~30-40% and improve GPU +throughput by ~1.5-2x for large molecules, with negligible impact on the +final energy. + +## Quick start + +Add a `[precision]` section to your TOML input file: + +```toml +[precision] +mode = "mixed" +``` + +Or keep the default (all float64, backward compatible): + +```toml +# [precision] section omitted → mode="full" +``` + +## Precision modes + +| Mode | Description | +|---------|-------------| +| `full` | All zones float64 (default, backward compatible) | +| `mixed` | Recommended mixed precision (see zone table below) | + +## Precision zones + +jQMC divides the computation into 10 **Precision Zones**, each independently +configurable: + +| Zone | Components | `full` | `mixed` | float32 risk | +|----------------|-----------------------------------|--------|---------|--------------| +| `orb_eval` | AO/MO forward evaluation | f64 | **f32** | low | +| `jastrow` | Jastrow factor (J1/J2/J3) | f64 | **f32** | low | +| `geminal` | Geminal matrix elements | f64 | **f32** | low | +| `determinant` | log-det, SVD, AS regularization | f64 | f64 | high | +| `coulomb` | Coulomb + ECP potential | f64 | **f32** | low-medium | +| `kinetic` | Kinetic energy + AO/MO derivatives | f64 | f64 | high | +| `mcmc` | MCMC sampling | f64 | f64 | high | +| `gfmc` | GFMC propagation | f64 | f64 | high | +| `optimization` | SR matrix, parameter updates | f64 | f64 | high | +| `io` | I/O, structure data | f64 | f64 | low-medium | + +## Custom zone configuration + +Individual zones can be overridden regardless of the base mode: + +```toml +[precision] +mode = "mixed" # start from recommended mixed defaults +orb_eval = "float64" # override: keep AO/MO in float64 +``` + +```toml +[precision] +mode = "full" # start from all float64 +orb_eval = "float32" # override: only AO/MO in float32 +``` + +## Workflow integration + +When using `jqmc_workflow`, pass precision settings to any workflow class: + +```python +from jqmc_workflow import VMC_Workflow + +wf = VMC_Workflow( + server_machine_name="cluster", + num_opt_steps=20, + precision_mode="mixed", +) +``` + +For custom per-zone overrides: + +```python +wf = VMC_Workflow( + server_machine_name="cluster", + num_opt_steps=20, + precision_mode="mixed", + precision_overrides={"orb_eval": "float64"}, +) +``` + +## Design principles + +1. **Explicit dtype declaration** — Every function declares its Precision Zone + and specifies dtype for all arrays. No reliance on JAX implicit promotion. + +2. **Zone boundaries** — When results cross zone boundaries (e.g. Jastrow + float32 → determinant float64), explicit casts ensure the higher-precision + zone receives correctly typed inputs. + +3. **Backward compatibility** — Default mode is `"full"` (all float64). + Existing input files work without modification. + +## API reference + +See {py:mod}`jqmc._precision` for the programmatic API (`get_dtype`, +`configure`, `get_tolerance`, etc.). diff --git a/examples/jqmc-example01/02vmc_JSD/vmc.toml b/examples/jqmc-example01/02vmc_JSD/vmc.toml index ceff4bea..1bcad5fd 100644 --- a/examples/jqmc-example01/02vmc_JSD/vmc.toml +++ b/examples/jqmc-example01/02vmc_JSD/vmc.toml @@ -28,3 +28,17 @@ opt_J3_basis_exp = true opt_J3_basis_coeff = true opt_lambda_basis_exp = false opt_lambda_basis_coeff = false + +# [precision] +# mode = "full" # "full" (default, all float64) or "mixed" (recommended mixed precision) +# # Per-zone overrides (optional, override the mode defaults): +# # orb_eval = "float32" # AO/MO forward evaluation +# # jastrow = "float32" # Jastrow factor +# # geminal = "float32" # Geminal matrix elements +# # determinant = "float64" # log-det, SVD, AS regularization +# # coulomb = "float32" # Coulomb + ECP potential +# # kinetic = "float64" # Kinetic energy + derivatives +# # mcmc = "float64" # MCMC sampling +# # gfmc = "float64" # GFMC propagation +# # optimization = "float64" # SR matrix, parameter updates +# # io = "float64" # I/O, structure data diff --git a/examples/jqmc-example01/03mcmc_JSD/mcmc.toml b/examples/jqmc-example01/03mcmc_JSD/mcmc.toml index 73a83362..295a0aef 100644 --- a/examples/jqmc-example01/03mcmc_JSD/mcmc.toml +++ b/examples/jqmc-example01/03mcmc_JSD/mcmc.toml @@ -14,3 +14,6 @@ num_mcmc_warmup_steps = 0 # Number of observable measurement steps for warmup (i num_mcmc_bin_blocks = 5 # Number of blocks for binning per MPI and Walker. i.e., the total number of binned blocks is num_mcmc_bin_blocks * mpi_size * number_of_walkers. Dt = 2.0 # Step size for the MCMC update (bohr). epsilon_AS = 0.0 # the epsilon parameter used in the Attacalite-Sandro regulatization method. + +# [precision] +# mode = "full" # "full" (default, all float64) or "mixed" (recommended mixed precision) diff --git a/examples/jqmc-example01/04lrdmc_JSD/lrdmc.toml b/examples/jqmc-example01/04lrdmc_JSD/lrdmc.toml index 903274a9..c8438fa9 100644 --- a/examples/jqmc-example01/04lrdmc_JSD/lrdmc.toml +++ b/examples/jqmc-example01/04lrdmc_JSD/lrdmc.toml @@ -17,3 +17,6 @@ num_gfmc_bin_blocks = 50 num_gfmc_collect_steps = 20 E_scf = -17.00 + +# [precision] +# mode = "full" # "full" (default, all float64) or "mixed" (recommended mixed precision) diff --git a/examples/jqmc-workflow-example01/run_pes_pipeline.py b/examples/jqmc-workflow-example01/run_pes_pipeline.py index cc27d2e2..46323bfd 100644 --- a/examples/jqmc-workflow-example01/run_pes_pipeline.py +++ b/examples/jqmc-workflow-example01/run_pes_pipeline.py @@ -51,6 +51,10 @@ TARGET_MCMC_ERROR = 5e-5 # Target statistical error (Ha) TARGET_LRDMC_ERROR = 5e-5 # Target statistical error (Ha) +# Mixed precision: set to "mixed" to enable float32 for low-risk zones, +# or None (default) for all-float64. See doc/notes/mixed_precision.md. +PRECISION_MODE = None # "mixed" or None + R_VALUES = [ 0.40, 0.45, @@ -281,6 +285,7 @@ def build_pipeline() -> tuple[list[Container], dict[float, Container], dict[floa max_time=3000, poll_interval=120, max_continuation=1, + precision_mode=PRECISION_MODE, ), ) @@ -305,6 +310,7 @@ def build_pipeline() -> tuple[list[Container], dict[float, Container], dict[floa max_time=3000, poll_interval=120, max_continuation=1, + precision_mode=PRECISION_MODE, ), ) @@ -330,6 +336,7 @@ def build_pipeline() -> tuple[list[Container], dict[float, Container], dict[floa max_time=3000, poll_interval=120, max_continuation=1, + precision_mode=PRECISION_MODE, ), ) diff --git a/jqmc/_jqmc_utility.py b/jqmc/_jqmc_utility.py index d4197f7a..6ce532fc 100644 --- a/jqmc/_jqmc_utility.py +++ b/jqmc/_jqmc_utility.py @@ -1,4 +1,10 @@ -"""utility module.""" +"""utility module. + +Precision Zones: + - ``io``: all functions in this module. + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -35,9 +41,12 @@ from functools import lru_cache from logging import getLogger +import jax.numpy as jnp import numpy as np import numpy.typing as npt +from ._precision import get_dtype + # set logger logger = getLogger("jqmc").getChild(__name__) @@ -93,6 +102,9 @@ def _generate_init_electron_configurations( min_dst = 0.1 max_dst = 1.0 + dtype = get_dtype("io") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + # 1) zeta[i] = integer valence count per atom nion = coords.shape[0] zeta = np.array([int(round(c)) for c in charges], dtype=int) @@ -120,8 +132,8 @@ def _generate_init_electron_configurations( i_prev = best_i # 4) Prepare storage for all walkers - r_carts_up = np.zeros((num_walkers, tot_num_electron_up, 3), dtype=float) - r_carts_dn = np.zeros((num_walkers, tot_num_electron_dn, 3), dtype=float) + r_carts_up = np.zeros((num_walkers, tot_num_electron_up, 3), dtype=dtype_np) + r_carts_dn = np.zeros((num_walkers, tot_num_electron_dn, 3), dtype=dtype_np) up_owner = np.zeros((num_walkers, tot_num_electron_up), dtype=int) dn_owner = np.zeros((num_walkers, tot_num_electron_dn), dtype=int) @@ -143,7 +155,7 @@ def _generate_init_electron_configurations( # Phase 1a: Place all down-electrons under Hund’s limit first # ----------------------------------------- ned_dn = tot_num_electron_dn - down_positions = np.zeros((ned_dn, 3), dtype=float) + down_positions = np.zeros((ned_dn, 3), dtype=dtype_np) j_counter = 0 for idn in range(ned_dn): @@ -209,7 +221,7 @@ def _generate_init_electron_configurations( sum_up_needed = int(np.sum(up_needed)) ned_up = tot_num_electron_up - up_positions = np.zeros((ned_up, 3), dtype=float) + up_positions = np.zeros((ned_up, 3), dtype=dtype_np) # Case 1: ned_up <= sum_up_needed → place ned_up among those up_needed slots if ned_up <= sum_up_needed: diff --git a/jqmc/_precision.py b/jqmc/_precision.py new file mode 100644 index 00000000..c4b942d0 --- /dev/null +++ b/jqmc/_precision.py @@ -0,0 +1,273 @@ +"""Mixed precision configuration for jQMC. + +Every computational function declares its Precision Zone and explicitly +specifies dtype for all variables it creates or consumes. This design +does NOT rely on JAX's implicit dtype propagation, ensuring robustness +against future changes in JAX's type promotion semantics. + +All zones are user-configurable. Defaults depend on the mode: + +- ``mode="full"`` (default) — all zones float64 (backward compatible). +- ``mode="mixed"`` — recommended mixed precision; low-risk zones become + float32 while numerically sensitive zones stay float64. + +Precision Zones +--------------- + +============== ============================ ========= ======== ============ +Zone Components Default Mixed float32 risk +============== ============================ ========= ======== ============ +``orb_eval`` AO/MO forward evaluation float64 float32 low +``jastrow`` Jastrow factor (J1/J2/J3) float64 float32 low +``geminal`` Geminal matrix elements float64 float32 low +``determinant`` log-det, SVD, AS reg. float64 float64 high +``coulomb`` Coulomb + ECP potential float64 float32 low-medium +``kinetic`` Kinetic energy + AO/MO derivs float64 float64 high +``mcmc`` MCMC sampling float64 float64 high +``gfmc`` GFMC propagation float64 float64 high +``optimization``SR matrix, parameter updates float64 float64 high +``io`` I/O, structure data float64 float64 low-medium +============== ============================ ========= ======== ============ + +File-to-zone mapping +-------------------- + +- ``atomic_orbital.py``: ``orb_eval`` (forward), ``kinetic`` (grad/laplacian) +- ``molecular_orbital.py``: ``orb_eval`` (forward), ``kinetic`` (grad/laplacian) +- ``jastrow_factor.py``: ``jastrow`` (forward), ``kinetic`` (grad/laplacian), + ``mcmc`` (ratio/update) +- ``determinant.py``: ``geminal`` (matrix elements), ``determinant`` (log-det/SVD), + ``kinetic`` (grad/laplacian) +- ``coulomb_potential.py``: ``coulomb`` +- ``wavefunction.py``: ``kinetic`` + zone-boundary casts +- ``hamiltonians.py``: ``kinetic`` (zone-boundary aggregation of T + V) +- ``jqmc_mcmc.py``: ``mcmc`` (sampling), ``optimization`` (SR/LM) +- ``jqmc_gfmc.py``: ``gfmc`` +- ``structure.py``, ``trexio_wrapper.py``, ``jqmc_tool.py``: ``io`` +- ``_jqmc_utility.py``: ``io`` +- ``swct.py``: ``kinetic`` + +Usage:: + + from jqmc._precision import get_dtype + + def compute_AOs(aos_data, r_carts): + dtype = get_dtype("orb_eval") + r_carts = jnp.asarray(r_carts, dtype=dtype) + ... +""" + +# Copyright (C) 2024- Kosuke Nakano +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# * Neither the name of the jqmc project nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import logging + +import jax.numpy as jnp + +logger = logging.getLogger(__name__) + +# --- mode="full" defaults (all float64, backward compatible) --- +_DEFAULTS_FULL: dict[str, str] = { + "orb_eval": "float64", # AO/MO forward evaluation + "jastrow": "float64", # Jastrow factor + "geminal": "float64", # Geminal matrix elements + "determinant": "float64", # log-det, SVD, AS regularization + "coulomb": "float64", # Coulomb + ECP potential + "kinetic": "float64", # Kinetic energy + AO/MO derivatives + "mcmc": "float64", # MCMC sampling (proposal, SM update, accept/reject, accumulation) + "gfmc": "float64", # GFMC propagation + "optimization": "float64", # SR matrix, parameter updates + "io": "float64", # I/O, structure data +} + +# --- mode="mixed" defaults (recommended mixed precision) --- +# float32 risk: +# orb_eval - low: smooth Gaussian basis + linear combination +# jastrow - low: smooth correlation function, pre-exp value +# geminal - low: building matrix elements only (pre-det) +# coulomb - low-medium: sum of 1/r + ECP spherical quadrature +# determinant - high: log(det) cancellation, SVD 1/s, eigenvalue ops +# kinetic - high: second derivative of ln|Psi|, cancellation-sensitive +# mcmc - high: SM inverse error accumulation, acceptance ratio, statistics +# gfmc - high: weighted branching/pruning, population collapse in float32 +# optimization- high: S^{-1}F linear system, ill-conditioned matrix +# io - low-medium: file I/O + nuclear coordinates +_DEFAULTS_MIXED: dict[str, str] = { + "orb_eval": "float32", # low risk + "jastrow": "float32", # low risk + "geminal": "float32", # low risk + "determinant": "float64", # high risk + "coulomb": "float32", # low-medium risk + "kinetic": "float64", # high risk + "mcmc": "float64", # high risk + "gfmc": "float64", # high risk + "optimization": "float64", # high risk + "io": "float64", # low-medium risk +} + +ALL_ZONES = frozenset(_DEFAULTS_FULL.keys()) + +# Runtime zone -> dtype mapping +_zone_dtypes: dict[str, type] = {} + + +def _str_to_dtype(s: str) -> type: + """Convert a string dtype name to the corresponding JAX/NumPy dtype type. + + Args: + s: Either ``"float32"`` or ``"float64"``. + + Returns: + ``jnp.float32`` or ``jnp.float64``. + + Raises: + ValueError: If *s* is not one of the two accepted strings. + """ + if s == "float32": + return jnp.float32 + elif s == "float64": + return jnp.float64 + else: + raise ValueError(f"Invalid dtype '{s}'. Must be 'float32' or 'float64'.") + + +def configure(precision_config: dict[str, str]) -> None: + """Set zone-level dtypes from a TOML ``[precision]`` section. + + Args: + precision_config: Mapping such as ``{"mode": "mixed", "orb_eval": "float32", ...}``. + + - ``mode="full"`` (default): all zones float64. + - ``mode="mixed"``: recommended mixed precision defaults. + - ``mode`` omitted: same as ``"full"``. + - Per-zone overrides take highest priority regardless of *mode*. + + Raises: + ValueError: If *mode* is unknown, a zone name is invalid, or a dtype + string is not ``"float32"``/``"float64"``. + """ + _zone_dtypes.clear() + + mode = precision_config.get("mode", "full") + + # Select base configuration + if mode == "full": + base = dict(_DEFAULTS_FULL) + elif mode == "mixed": + base = dict(_DEFAULTS_MIXED) + else: + raise ValueError(f"Unknown precision mode '{mode}'. Must be 'full' or 'mixed'.") + + # Apply per-zone overrides + for zone, dtype_str in precision_config.items(): + if zone == "mode": + continue + if zone not in ALL_ZONES: + raise ValueError(f"Unknown precision zone '{zone}'. Available zones: {sorted(ALL_ZONES)}") + _str_to_dtype(dtype_str) # validate + base[zone] = dtype_str + + # Build final mapping + for zone, dtype_str in base.items(): + _zone_dtypes[zone] = _str_to_dtype(dtype_str) + + # Log the precision configuration + logger.info(summary()) + + +def get_dtype(zone: str) -> type: + """Return the dtype for a Precision Zone. + + When :func:`configure` has not been called, ``jnp.float64`` is returned + for any zone name (backward compatible). After :func:`configure` has + been called, an unknown *zone* raises :class:`ValueError`. + + Args: + zone: Precision Zone name (e.g. ``"orb_eval"``, ``"kinetic"``). + + Returns: + ``jnp.float32`` or ``jnp.float64``. + """ + if _zone_dtypes: + # configure() has been called: validate zone name + if zone not in ALL_ZONES: + raise ValueError(f"Unknown precision zone '{zone}'. Available zones: {sorted(ALL_ZONES)}") + return _zone_dtypes.get(zone, jnp.float64) + + +def is_mixed_precision_enabled() -> bool: + """Return ``True`` if at least one zone is set to float32.""" + return any(d == jnp.float32 for d in _zone_dtypes.values()) + + +def get_tolerance(zone: str, level: str = "strict") -> tuple[float, float]: + """Return test tolerances ``(atol, rtol)`` scaled by the zone's dtype. + + Args: + zone: Precision Zone name. + level: ``"strict"`` or ``"loose"``. + + Returns: + ``(atol, rtol)`` tuple appropriate for the zone's current dtype. + """ + from jqmc._setting import _TOLERANCE + + dtype_key = "float32" if get_dtype(zone) == jnp.float32 else "float64" + return _TOLERANCE[level][dtype_key] + + +def get_tolerance_min(zones, level: str = "strict") -> tuple[float, float]: + """Return ``(atol, rtol)`` loose enough for the lowest-precision zone. + + Use for comparing two exact computations whose path crosses + multiple zones; the achievable agreement is bounded by the + weakest (largest tolerance) zone on the path. + + Args: + zones: Iterable of Precision Zone names. + level: ``"strict"`` or ``"loose"``. + + Returns: + ``(atol, rtol)`` tuple using the maximum of each component. + """ + atols, rtols = zip(*(get_tolerance(z, level) for z in zones)) + return max(atols), max(rtols) + + +def summary() -> str: + """Return a human-readable summary of the current precision configuration.""" + lines = ["Precision configuration:"] + for zone in sorted(ALL_ZONES): + dtype = get_dtype(zone) + tag = "float32" if dtype == jnp.float32 else "float64" + lines.append(f" {zone}: {tag}") + return "\n".join(lines) diff --git a/jqmc/_setting.py b/jqmc/_setting.py index 8b4df573..bdf2b60a 100644 --- a/jqmc/_setting.py +++ b/jqmc/_setting.py @@ -1,4 +1,8 @@ -"""setting.""" +"""Setting module. + +Contains physical constants, numerical stability parameters, and +test tolerance definitions used across jQMC. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -32,6 +36,8 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import numpy as np + # Unit conversion Bohr_to_Angstrom = 0.529177210903 Angstrom_to_Bohr = 1.0 / Bohr_to_Angstrom @@ -68,6 +74,58 @@ atol_consistency = 1.0e-8 rtol_consistency = 1.0e-6 +# --- Test tolerance dict (dtype-aware) --- +# +# Accessed via ``_precision.get_tolerance(zone, level)`` which resolves the +# zone's current dtype and returns ``(atol, rtol)``. +# +# Levels: +# strict — two exact implementations of the same quantity (debug vs +# production, analytic vs autodiff). Difference is pure +# floating-point round-off. +# loose — comparison involving numerical differentiation or quadrature. +# Finite-difference truncation error dominates, so tolerances +# are much wider. +_TOLERANCE: dict[str, dict[str, tuple[float, float]]] = { + "strict": {"float64": (1e-8, 1e-6), "float32": (1e-5, 1e-3)}, + "loose": {"float64": (1e-1, 1e-4), "float32": (1e-1, 1e-3)}, +} + +# --- Dtype-aware EPS constants --- +# +# Some EPS values are tuned for float64 and break under float32 (underflow, +# loss of stabilization). Use ``get_eps(name, dtype)`` to obtain the +# appropriate value for the current precision zone. +# +# Constants: +# machine_precision — floor for safe ratio in diagnostics. +# stabilizing_ao — small epsilon for AO Cartesian derivative stabilization. +# rcond_svd — threshold for SVD pseudoinverse of the geminal matrix. +_EPS_DTYPE_AWARE: dict[str, dict[str, float]] = { + "machine_precision": {"float64": 1e-300, "float32": 1e-38}, + "stabilizing_ao": {"float64": 1e-16, "float32": 1e-7}, + "rcond_svd": {"float64": 1e-20, "float32": 1e-6}, +} + + +def get_eps(name: str, dtype) -> float: + """Return a dtype-aware numerical stability constant. + + Args: + name: One of ``"machine_precision"``, ``"stabilizing_ao"``, + ``"rcond_svd"``. + dtype: A NumPy/JAX dtype (e.g. ``jnp.float32``, ``np.float64``). + + Returns: + The appropriate epsilon value for the given dtype. + + Raises: + KeyError: If *name* is not a known EPS constant. + """ + dtype_key = "float32" if np.dtype(dtype) == np.float32 else "float64" + return _EPS_DTYPE_AWARE[name][dtype_key] + + # Numerical stability settings for AO EPS_stabilizing_jax_AO_cart_deriv = 1.0e-16 diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index 76fe62fe..ce61982f 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -2,6 +2,11 @@ Module containing classes and methods related to Atomic Orbitals +Precision Zones: + - ``orb_eval``: forward AO evaluation (compute_AOs and internal helpers). + - ``kinetic``: AO gradient and Laplacian (compute_AOs_grad, compute_AOs_laplacian). + +See :mod:`jqmc._precision` for details. """ from __future__ import annotations @@ -52,6 +57,7 @@ from numpy import linalg as LA from ._jqmc_utility import _spherical_to_cart_matrix +from ._precision import get_dtype from ._setting import EPS_stabilizing_jax_AO_cart_deriv, atol_consistency, rtol_consistency from .structure import Structure_data @@ -701,6 +707,7 @@ def _polynominal_order_z_prim_jnp(self) -> jax.Array: @property def _normalization_factorial_ratio_prim_jnp(self) -> jax.Array: """Return factorial ratio used in AO normalization (primitive-wise).""" + dtype = get_dtype("io") nx = self._polynominal_order_x_prim_np ny = self._polynominal_order_y_prim_np nz = self._polynominal_order_z_prim_np @@ -714,18 +721,21 @@ def _normalization_factorial_ratio_prim_jnp(self) -> jax.Array: * scipy.special.factorial(2 * ny, exact=True) * scipy.special.factorial(2 * nz, exact=True) ) - ratio = np.asarray(num / den, dtype=np.float64) - return jnp.array(ratio, dtype=jnp.float64) + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + ratio = np.asarray(num / den, dtype=dtype_np) + return jnp.array(ratio, dtype=dtype) @property def _exponents_jnp(self) -> jax.Array: """Return exponents.""" - return jnp.asarray(self.exponents, dtype=jnp.float64) + dtype = get_dtype("io") + return jnp.asarray(self.exponents, dtype=dtype) @property def _coefficients_jnp(self) -> jax.Array: """Return coefficients.""" - return jnp.asarray(self.coefficients, dtype=jnp.float64) + dtype = get_dtype("io") + return jnp.asarray(self.coefficients, dtype=dtype) @property def _num_orb(self) -> int: @@ -1268,12 +1278,14 @@ def _magnetic_quantum_numbers_prim_jnp(self) -> jax.Array: @property def _exponents_jnp(self) -> jax.Array: """Return exponents.""" - return jnp.asarray(self.exponents, dtype=jnp.float64) + dtype = get_dtype("io") + return jnp.asarray(self.exponents, dtype=dtype) @property def _coefficients_jnp(self) -> jax.Array: """Return coefficients.""" - return jnp.asarray(self.coefficients, dtype=jnp.float64) + dtype = get_dtype("io") + return jnp.asarray(self.coefficients, dtype=dtype) @property def _num_orb(self) -> int: @@ -1340,12 +1352,14 @@ def symmetrize(self, arr: np.ndarray) -> np.ndarray: @classmethod def from_aos_data(cls, aos_data: "AOs_sphe_data | AOs_cart_data") -> "ShellPrimMap": """Build a shell map from an AO dataclass instance.""" + dtype = get_dtype("io") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 ao_prims: dict[int, list[int]] = {} for prim_idx, ao_idx in enumerate(aos_data.orbital_indices): ao_prims.setdefault(ao_idx, []).append(prim_idx) - exps_np = np.asarray(aos_data.exponents, dtype=np.float64) - coefs_np = np.asarray(aos_data.coefficients, dtype=np.float64) + exps_np = np.asarray(aos_data.exponents, dtype=dtype_np) + coefs_np = np.asarray(aos_data.coefficients, dtype=dtype_np) shells: list[tuple[int, list[int]]] = [] _shell_keys: list[tuple] = [] @@ -1406,8 +1420,10 @@ def _aos_sphe_to_cart(aos_data: AOs_sphe_data | AOs_cart_data) -> tuple[AOs_cart tuple: (AOs_cart_data, transform_matrix) where transform_matrix maps spherical -> Cartesian coefficients with shape (num_ao_sph, num_ao_cart). """ + dtype = get_dtype("orb_eval") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 if isinstance(aos_data, AOs_cart_data): - transform_matrix = np.eye(aos_data.num_ao, dtype=np.float64) + transform_matrix = np.eye(aos_data.num_ao, dtype=dtype_np) return aos_data, transform_matrix if not isinstance(aos_data, AOs_sphe_data): raise ValueError("Cartesian conversion is only available from spherical AOs.") @@ -1426,8 +1442,8 @@ def _match_shell(existing: dict, nucleus: int, l: int, exps: np.ndarray, coefs: for ao_idx in range(aos_sphe.num_ao): prim_indices = [i for i, v in enumerate(aos_sphe.orbital_indices) if v == ao_idx] - exps = np.asarray([aos_sphe.exponents[i] for i in prim_indices], dtype=np.float64) - coefs = np.asarray([aos_sphe.coefficients[i] for i in prim_indices], dtype=np.float64) + exps = np.asarray([aos_sphe.exponents[i] for i in prim_indices], dtype=dtype_np) + coefs = np.asarray([aos_sphe.coefficients[i] for i in prim_indices], dtype=dtype_np) nucleus = aos_sphe.nucleus_index[ao_idx] l = aos_sphe.angular_momentums[ao_idx] @@ -1440,7 +1456,7 @@ def _match_shell(existing: dict, nucleus: int, l: int, exps: np.ndarray, coefs: total_cart = sum((shell["l"] + 1) * (shell["l"] + 2) // 2 for shell in shells) total_sph = aos_sphe.num_ao - transform_matrix = np.zeros((total_sph, total_cart), dtype=np.float64) + transform_matrix = np.zeros((total_sph, total_cart), dtype=dtype_np) new_nucleus_index: list[int] = [] new_angular_momentums: list[int] = [] @@ -1485,8 +1501,8 @@ def _match_shell(existing: dict, nucleus: int, l: int, exps: np.ndarray, coefs: num_ao=total_cart, num_ao_prim=len(new_exponents), orbital_indices=new_orbital_indices, - exponents=jnp.array(new_exponents, dtype=jnp.float64), - coefficients=jnp.array(new_coefficients, dtype=jnp.float64), + exponents=jnp.array(new_exponents, dtype=dtype), + coefficients=jnp.array(new_coefficients, dtype=dtype), angular_momentums=new_angular_momentums, polynominal_order_x=new_polynominal_order_x, polynominal_order_y=new_polynominal_order_y, @@ -1503,8 +1519,10 @@ def _aos_cart_to_sphe(aos_data: AOs_cart_data | AOs_sphe_data) -> tuple[AOs_sphe tuple: (AOs_sphe_data, transform_pinv) where transform_pinv maps Cartesian -> spherical coefficients with shape (num_ao_cart, num_ao_sph). """ + dtype = get_dtype("orb_eval") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 if isinstance(aos_data, AOs_sphe_data): - transform_pinv = np.eye(aos_data.num_ao, dtype=np.float64) + transform_pinv = np.eye(aos_data.num_ao, dtype=dtype_np) return aos_data, transform_pinv if not isinstance(aos_data, AOs_cart_data): raise ValueError("Spherical conversion is only available from Cartesian AOs.") @@ -1523,8 +1541,8 @@ def _match_shell(existing: dict, nucleus: int, l: int, exps: np.ndarray, coefs: for ao_idx in range(aos_cart.num_ao): prim_indices = [i for i, v in enumerate(aos_cart.orbital_indices) if v == ao_idx] - exps = np.asarray([aos_cart.exponents[i] for i in prim_indices], dtype=np.float64) - coefs = np.asarray([aos_cart.coefficients[i] for i in prim_indices], dtype=np.float64) + exps = np.asarray([aos_cart.exponents[i] for i in prim_indices], dtype=dtype_np) + coefs = np.asarray([aos_cart.coefficients[i] for i in prim_indices], dtype=dtype_np) nucleus = aos_cart.nucleus_index[ao_idx] l = aos_cart.angular_momentums[ao_idx] order = ( @@ -1550,7 +1568,7 @@ def _match_shell(existing: dict, nucleus: int, l: int, exps: np.ndarray, coefs: total_sph = sum(2 * shell["l"] + 1 for shell in shells) total_cart = aos_cart.num_ao - transform_matrix = np.zeros((total_sph, total_cart), dtype=np.float64) + transform_matrix = np.zeros((total_sph, total_cart), dtype=dtype_np) new_nucleus_index: list[int] = [] new_angular_momentums: list[int] = [] @@ -1598,8 +1616,8 @@ def _match_shell(existing: dict, nucleus: int, l: int, exps: np.ndarray, coefs: num_ao=total_sph, num_ao_prim=len(new_exponents), orbital_indices=new_orbital_indices, - exponents=jnp.array(new_exponents, dtype=jnp.float64), - coefficients=jnp.array(new_coefficients, dtype=jnp.float64), + exponents=jnp.array(new_exponents, dtype=dtype), + coefficients=jnp.array(new_coefficients, dtype=dtype), angular_momentums=new_angular_momentums, magnetic_quantum_numbers=new_magnetic_quantum_numbers, ) @@ -1650,18 +1668,20 @@ def _compute_overlap_1d_cart( def _compute_overlap_matrix_cart_analytic(aos_cart_data: AOs_cart_data) -> npt.NDArray[np.float64]: """Compute AO overlap matrix analytically for Cartesian contracted GTOs.""" + dtype = get_dtype("orb_eval") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 num_ao = aos_cart_data.num_ao - overlap_matrix = np.zeros((num_ao, num_ao), dtype=np.float64) + overlap_matrix = np.zeros((num_ao, num_ao), dtype=dtype_np) - centers = np.asarray(aos_cart_data._atomic_center_carts_np, dtype=np.float64) + centers = np.asarray(aos_cart_data._atomic_center_carts_np, dtype=dtype_np) orbital_indices = list(aos_cart_data.orbital_indices) primitive_indices_by_ao: list[list[int]] = [[] for _ in range(num_ao)] for primitive_index, ao_index in enumerate(orbital_indices): primitive_indices_by_ao[ao_index].append(primitive_index) - exponents = np.asarray(aos_cart_data.exponents, dtype=np.float64) - coefficients = np.asarray(aos_cart_data.coefficients, dtype=np.float64) + exponents = np.asarray(aos_cart_data.exponents, dtype=dtype_np) + coefficients = np.asarray(aos_cart_data.coefficients, dtype=dtype_np) nx = np.asarray(aos_cart_data.polynominal_order_x, dtype=np.int32) ny = np.asarray(aos_cart_data.polynominal_order_y, dtype=np.int32) @@ -1670,7 +1690,7 @@ def _compute_overlap_matrix_cart_analytic(aos_cart_data: AOs_cart_data) -> npt.N normalization = np.sqrt( (2.0 * exponents / np.pi) ** (3.0 / 2.0) * (8.0 * exponents) ** np.asarray(aos_cart_data._angular_momentums_prim_np, dtype=np.int32) - * np.asarray(aos_cart_data._normalization_factorial_ratio_prim_jnp, dtype=np.float64) + * np.asarray(aos_cart_data._normalization_factorial_ratio_prim_jnp, dtype=dtype_np) ) for mu in range(num_ao): @@ -1720,11 +1740,13 @@ def _estimate_overlap_integration_box( tail_tolerance: float = 1.0e-11, ) -> tuple[np.ndarray, np.ndarray]: """Estimate finite integration bounds for numerical overlap integration.""" + dtype = get_dtype("orb_eval") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 if tail_tolerance <= 0.0 or tail_tolerance >= 1.0: raise ValueError(f"tail_tolerance must satisfy 0 < tail_tolerance < 1. Got {tail_tolerance}.") - centers = np.asarray(aos_data.structure_data.positions, dtype=np.float64) - exponents = np.asarray(aos_data.exponents, dtype=np.float64) + centers = np.asarray(aos_data.structure_data.positions, dtype=dtype_np) + exponents = np.asarray(aos_data.exponents, dtype=dtype_np) positive_exponents = exponents[exponents > 0.0] if positive_exponents.size == 0: raise ValueError("All AO exponents are non-positive. Cannot estimate integration bounds.") @@ -1744,6 +1766,8 @@ def _build_overlap_integration_grid( tail_tolerance: float, ) -> tuple[np.ndarray, float]: """Build a uniform midpoint grid and volume element for numerical overlap integration.""" + dtype = get_dtype("orb_eval") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 if num_grid_points < 3: raise ValueError(f"num_grid_points must be >= 3. Got {num_grid_points}.") @@ -1751,9 +1775,9 @@ def _build_overlap_integration_grid( lengths = upper - lower dx, dy, dz = lengths / float(num_grid_points) - x = lower[0] + (np.arange(num_grid_points, dtype=np.float64) + 0.5) * dx - y = lower[1] + (np.arange(num_grid_points, dtype=np.float64) + 0.5) * dy - z = lower[2] + (np.arange(num_grid_points, dtype=np.float64) + 0.5) * dz + x = lower[0] + (np.arange(num_grid_points, dtype=dtype_np) + 0.5) * dx + y = lower[1] + (np.arange(num_grid_points, dtype=dtype_np) + 0.5) * dy + z = lower[2] + (np.arange(num_grid_points, dtype=dtype_np) + 0.5) * dz xx, yy, zz = np.meshgrid(x, y, z, indexing="ij") r_carts = np.stack([xx, yy, zz], axis=-1).reshape((-1, 3)) @@ -1768,13 +1792,15 @@ def _compute_overlap_matrix_debug( tail_tolerance: float = 1.0e-11, ) -> npt.NDArray[np.float64]: """Numerically compute AO overlap matrix by 3D midpoint integration (debug).""" + dtype = get_dtype("orb_eval") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 r_carts, volume_element = _build_overlap_integration_grid( aos_data=aos_data, num_grid_points=num_grid_points, tail_tolerance=tail_tolerance, ) - ao_values = np.asarray(_compute_AOs_debug(aos_data=aos_data, r_carts=r_carts), dtype=np.float64) + ao_values = np.asarray(_compute_AOs_debug(aos_data=aos_data, r_carts=r_carts), dtype=dtype_np) overlap_matrix = np.dot(ao_values, ao_values.T) * volume_element overlap_matrix = 0.5 * (overlap_matrix + overlap_matrix.T) return overlap_matrix @@ -1786,6 +1812,7 @@ def compute_overlap_matrix(aos_data: AOs_sphe_data | AOs_cart_data) -> jax.Array For spherical AOs, the overlap is evaluated by conversion to Cartesian AOs and transformed back with the spherical-to-Cartesian matrix. """ + dtype = get_dtype("orb_eval") aos_cart_data, transform_matrix = _aos_sphe_to_cart(aos_data) cart_overlap_matrix = _compute_overlap_matrix_cart_analytic(aos_cart_data) @@ -1797,7 +1824,7 @@ def compute_overlap_matrix(aos_data: AOs_sphe_data | AOs_cart_data) -> jax.Array raise NotImplementedError overlap_matrix = 0.5 * (overlap_matrix + overlap_matrix.T) - return jnp.asarray(overlap_matrix, dtype=jnp.float64) + return jnp.asarray(overlap_matrix, dtype=dtype) def compute_AOs(aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax.Array) -> jax.Array: @@ -1818,7 +1845,8 @@ def compute_AOs(aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax.Array) -> Raises: NotImplementedError: If ``aos_data`` is neither Cartesian nor spherical. """ - r_carts = jnp.asarray(r_carts, dtype=jnp.float64) + dtype = get_dtype("orb_eval") + r_carts = jnp.asarray(r_carts, dtype=dtype) if isinstance(aos_data, AOs_sphe_data): AOs = _compute_AOs_sphe(aos_data, r_carts) @@ -1848,6 +1876,8 @@ def _compute_AOs_sphe_debug(aos_data: AOs_sphe_data, r_carts: npt.NDArray[np.flo The method is for computing the value of the given atomic orbital at r_carts for debugging purpose. See compute_AOs_api. """ + dtype = get_dtype("orb_eval") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 aos_values = [] for ao_index in range(aos_data.num_ao): @@ -1901,6 +1931,8 @@ def _compute_AOs_cart_debug(aos_data: AOs_cart_data, r_carts: npt.NDArray[np.flo The method is for computing the value of the given atomic orbital at r_carts for debugging purpose. See compute_AOs_api. """ + dtype = get_dtype("orb_eval") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 aos_values = [] for ao_index in range(aos_data.num_ao): @@ -2285,39 +2317,40 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j tuple: (values, grads, laps) where values has shape (49, num_R, num_r), grads has shape (49, num_R, num_r, 3), and laps has shape (49, num_R, num_r). """ + dtype = get_dtype("kinetic") S_L_M_COEFFS = ( - jnp.array([1.0], dtype=jnp.float64), - jnp.array([1.0], dtype=jnp.float64), - jnp.array([1.0], dtype=jnp.float64), - jnp.array([1.0], dtype=jnp.float64), - jnp.array([1.7320508075688774], dtype=jnp.float64), - jnp.array([1.7320508075688774], dtype=jnp.float64), - jnp.array([1.0, -0.5, -0.5], dtype=jnp.float64), - jnp.array([1.7320508075688774], dtype=jnp.float64), - jnp.array([-0.8660254037844387, 0.8660254037844387], dtype=jnp.float64), - jnp.array([-0.7905694150420949, 2.3717082451262845], dtype=jnp.float64), - jnp.array([3.8729833462074166], dtype=jnp.float64), - jnp.array([2.4494897427831783, -0.6123724356957946, -0.6123724356957946], dtype=jnp.float64), - jnp.array([1.0, -1.5, -1.5], dtype=jnp.float64), - jnp.array([2.4494897427831783, -0.6123724356957946, -0.6123724356957946], dtype=jnp.float64), - jnp.array([-1.9364916731037083, 1.9364916731037083], dtype=jnp.float64), - jnp.array([-2.3717082451262845, 0.7905694150420949], dtype=jnp.float64), - jnp.array([-2.958039891549808, 2.958039891549808], dtype=jnp.float64), - jnp.array([-2.091650066335189, 6.274950199005566], dtype=jnp.float64), - jnp.array([6.708203932499371, -1.1180339887498951, -1.1180339887498951], dtype=jnp.float64), - jnp.array([3.1622776601683795, -2.3717082451262845, -2.3717082451262845], dtype=jnp.float64), - jnp.array([1.0, -3.0, 0.375, -3.0, 0.75, 0.375], dtype=jnp.float64), - jnp.array([3.1622776601683795, -2.3717082451262845, -2.3717082451262845], dtype=jnp.float64), - jnp.array([-3.3541019662496856, 0.5590169943749476, 3.3541019662496856, -0.5590169943749476], dtype=jnp.float64), - jnp.array([-6.274950199005566, 2.091650066335189], dtype=jnp.float64), - jnp.array([0.739509972887452, -4.437059837324712, 0.739509972887452], dtype=jnp.float64), - jnp.array([0.7015607600201141, -7.015607600201141, 3.5078038001005707], dtype=jnp.float64), - jnp.array([-8.874119674649426, 8.874119674649426], dtype=jnp.float64), + jnp.array([1.0], dtype=dtype), + jnp.array([1.0], dtype=dtype), + jnp.array([1.0], dtype=dtype), + jnp.array([1.0], dtype=dtype), + jnp.array([1.7320508075688774], dtype=dtype), + jnp.array([1.7320508075688774], dtype=dtype), + jnp.array([1.0, -0.5, -0.5], dtype=dtype), + jnp.array([1.7320508075688774], dtype=dtype), + jnp.array([-0.8660254037844387, 0.8660254037844387], dtype=dtype), + jnp.array([-0.7905694150420949, 2.3717082451262845], dtype=dtype), + jnp.array([3.8729833462074166], dtype=dtype), + jnp.array([2.4494897427831783, -0.6123724356957946, -0.6123724356957946], dtype=dtype), + jnp.array([1.0, -1.5, -1.5], dtype=dtype), + jnp.array([2.4494897427831783, -0.6123724356957946, -0.6123724356957946], dtype=dtype), + jnp.array([-1.9364916731037083, 1.9364916731037083], dtype=dtype), + jnp.array([-2.3717082451262845, 0.7905694150420949], dtype=dtype), + jnp.array([-2.958039891549808, 2.958039891549808], dtype=dtype), + jnp.array([-2.091650066335189, 6.274950199005566], dtype=dtype), + jnp.array([6.708203932499371, -1.1180339887498951, -1.1180339887498951], dtype=dtype), + jnp.array([3.1622776601683795, -2.3717082451262845, -2.3717082451262845], dtype=dtype), + jnp.array([1.0, -3.0, 0.375, -3.0, 0.75, 0.375], dtype=dtype), + jnp.array([3.1622776601683795, -2.3717082451262845, -2.3717082451262845], dtype=dtype), + jnp.array([-3.3541019662496856, 0.5590169943749476, 3.3541019662496856, -0.5590169943749476], dtype=dtype), + jnp.array([-6.274950199005566, 2.091650066335189], dtype=dtype), + jnp.array([0.739509972887452, -4.437059837324712, 0.739509972887452], dtype=dtype), + jnp.array([0.7015607600201141, -7.015607600201141, 3.5078038001005707], dtype=dtype), + jnp.array([-8.874119674649426, 8.874119674649426], dtype=dtype), jnp.array( [-4.183300132670378, 0.5229125165837972, 12.549900398011133, -1.0458250331675945, -1.5687375497513916], - dtype=jnp.float64, + dtype=dtype, ), - jnp.array([10.2469507659596, -5.1234753829798, -5.1234753829798], dtype=jnp.float64), + jnp.array([10.2469507659596, -5.1234753829798, -5.1234753829798], dtype=dtype), jnp.array( [ 3.872983346207417, @@ -2327,9 +2360,9 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j 0.9682458365518543, 0.4841229182759271, ], - dtype=jnp.float64, + dtype=dtype, ), - jnp.array([1.0, -5.0, 1.875, -5.0, 3.75, 1.875], dtype=jnp.float64), + jnp.array([1.0, -5.0, 1.875, -5.0, 3.75, 1.875], dtype=dtype), jnp.array( [ 3.872983346207417, @@ -2339,21 +2372,21 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j 0.9682458365518543, 0.4841229182759271, ], - dtype=jnp.float64, + dtype=dtype, ), - jnp.array([-5.1234753829798, 2.5617376914899, 5.1234753829798, -2.5617376914899], dtype=jnp.float64), + jnp.array([-5.1234753829798, 2.5617376914899, 5.1234753829798, -2.5617376914899], dtype=dtype), jnp.array( [-12.549900398011133, 1.5687375497513916, 4.183300132670378, 1.0458250331675945, -0.5229125165837972], - dtype=jnp.float64, + dtype=dtype, ), - jnp.array([2.2185299186623566, -13.311179511974139, 2.2185299186623566], dtype=jnp.float64), - jnp.array([3.5078038001005707, -7.015607600201141, 0.7015607600201141], dtype=jnp.float64), - jnp.array([4.030159736288377, -13.433865787627923, 4.030159736288377], dtype=jnp.float64), - jnp.array([2.3268138086232857, -23.268138086232856, 11.634069043116428], dtype=jnp.float64), - jnp.array([-19.843134832984433, 1.9843134832984433, 19.843134832984433, -1.9843134832984433], dtype=jnp.float64), + jnp.array([2.2185299186623566, -13.311179511974139, 2.2185299186623566], dtype=dtype), + jnp.array([3.5078038001005707, -7.015607600201141, 0.7015607600201141], dtype=dtype), + jnp.array([4.030159736288377, -13.433865787627923, 4.030159736288377], dtype=dtype), + jnp.array([2.3268138086232857, -23.268138086232856, 11.634069043116428], dtype=dtype), + jnp.array([-19.843134832984433, 1.9843134832984433, 19.843134832984433, -1.9843134832984433], dtype=dtype), jnp.array( [-7.245688373094719, 2.7171331399105196, 21.737065119284157, -5.434266279821039, -8.15139941973156], - dtype=jnp.float64, + dtype=dtype, ), jnp.array( [ @@ -2364,16 +2397,16 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j 1.8114220932736798, 0.9057110466368399, ], - dtype=jnp.float64, + dtype=dtype, ), jnp.array( [4.58257569495584, -11.4564392373896, 2.8641098093474, -11.4564392373896, 5.7282196186948, 2.8641098093474], - dtype=jnp.float64, + dtype=dtype, ), - jnp.array([1.0, -7.5, 5.625, -0.3125, -7.5, 11.25, -0.9375, 5.625, -0.9375, -0.3125], dtype=jnp.float64), + jnp.array([1.0, -7.5, 5.625, -0.3125, -7.5, 11.25, -0.9375, 5.625, -0.9375, -0.3125], dtype=dtype), jnp.array( [4.58257569495584, -11.4564392373896, 2.8641098093474, -11.4564392373896, 5.7282196186948, 2.8641098093474], - dtype=jnp.float64, + dtype=dtype, ), jnp.array( [ @@ -2386,11 +2419,11 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j 0.45285552331841994, 0.45285552331841994, ], - dtype=jnp.float64, + dtype=dtype, ), jnp.array( [-21.737065119284157, 8.15139941973156, 7.245688373094719, 5.434266279821039, -2.7171331399105196], - dtype=jnp.float64, + dtype=dtype, ), jnp.array( [ @@ -2402,10 +2435,10 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j 2.480391854123054, -0.4960783708246108, ], - dtype=jnp.float64, + dtype=dtype, ), - jnp.array([11.634069043116428, -23.268138086232856, 2.3268138086232857], dtype=jnp.float64), - jnp.array([-0.6716932893813962, 10.075399340720942, -10.075399340720942, 0.6716932893813962], dtype=jnp.float64), + jnp.array([11.634069043116428, -23.268138086232856, 2.3268138086232857], dtype=dtype), + jnp.array([-0.6716932893813962, 10.075399340720942, -10.075399340720942, 0.6716932893813962], dtype=dtype), ) S_L_M_EXPS = ( @@ -2531,6 +2564,7 @@ def _single_val_grad_lap(diff: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.A @jit def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for Cartesian AOs (contracted).""" + dtype = get_dtype("kinetic") r_carts = jnp.asarray(r_carts) R_carts = aos_data._atomic_center_carts_prim_jnp c = aos_data._coefficients_jnp @@ -2575,6 +2609,7 @@ def _second_component(base, n): @jit def _compute_AOs_laplacian_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for spherical AOs (contracted).""" + dtype = get_dtype("kinetic") r_carts = jnp.asarray(r_carts) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp R_carts_jnp = aos_data._atomic_center_carts_prim_jnp @@ -2584,8 +2619,8 @@ def _compute_AOs_laplacian_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.n l_jnp = aos_data._angular_momentums_prim_jnp m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp - l_f64 = l_jnp.astype(jnp.float64) - Z_f64 = Z_jnp.astype(jnp.float64) + l_f64 = l_jnp.astype(dtype) + Z_f64 = Z_jnp.astype(dtype) factorial_l_plus_1 = jnp.exp(jscipy.special.gammaln(l_f64 + 2.0)) factorial_2l_plus_2 = jnp.exp(jscipy.special.gammaln(2.0 * l_f64 + 3.0)) @@ -2646,7 +2681,8 @@ def compute_AOs_laplacian(aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax. Raises: NotImplementedError: If ``aos_data`` is not Cartesian or spherical. """ - r_carts = jnp.asarray(r_carts, dtype=jnp.float64) + dtype = get_dtype("kinetic") + r_carts = jnp.asarray(r_carts, dtype=dtype) if isinstance(aos_data, AOs_cart_data): return _compute_AOs_laplacian_analytic_cart(aos_data, r_carts) @@ -2700,9 +2736,11 @@ def _compute_S_l_m_debug( They can be hardcoded into a code, or they can be computed analytically (e.g., https://en.wikipedia.org/wiki/Solid_harmonics). The latter one is the strategy employed in this code, """ + dtype = get_dtype("orb_eval") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 R_cart = atomic_center_cart - x, y, z = np.array(r_cart) - np.array(R_cart) - r_norm = LA.norm(np.array(r_cart) - np.array(R_cart)) + x, y, z = np.array(r_cart, dtype=dtype_np) - np.array(R_cart, dtype=dtype_np) + r_norm = LA.norm(np.array(r_cart, dtype=dtype_np) - np.array(R_cart, dtype=dtype_np)) l, m = angular_momentum, magnetic_quantum_number m_abs = np.abs(m) @@ -2756,6 +2794,7 @@ def _compute_AOs_laplacian_autodiff(aos_data: AOs_sphe_data | AOs_cart_data, r_c See compute_AOs_laplacian_api """ + dtype = get_dtype("kinetic") # noqa: F841 # not very fast, but it works. ao_matrix_hessian = hessian(compute_AOs, argnums=1)(aos_data, r_carts) ao_matrix_laplacian = jnp.einsum("m i i u i u -> mi", ao_matrix_hessian) @@ -2781,6 +2820,8 @@ def _compute_AOs_laplacian_debug( Array containing laplacians of the AOs at r_carts. The dim. is (num_ao, N_e) """ + dtype = get_dtype("kinetic") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 # noqa: F841 # Laplacians of AOs (numerical) diff_h = 1.0e-5 @@ -2829,6 +2870,7 @@ def _compute_AOs_laplacian_debug( @jit def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for Cartesian AOs (contracted).""" + dtype = get_dtype("kinetic") # noqa: F841 r_carts = jnp.asarray(r_carts) R_carts = aos_data._atomic_center_carts_prim_jnp c = aos_data._coefficients_jnp @@ -2876,6 +2918,7 @@ def _grad_component(base, n): @jit def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for spherical AOs (contracted).""" + dtype = get_dtype("kinetic") r_carts = jnp.asarray(r_carts) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp R_carts_jnp = aos_data._atomic_center_carts_prim_jnp @@ -2885,8 +2928,8 @@ def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarra l_jnp = aos_data._angular_momentums_prim_jnp m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp - l_f64 = l_jnp.astype(jnp.float64) - Z_f64 = Z_jnp.astype(jnp.float64) + l_f64 = l_jnp.astype(dtype) + Z_f64 = Z_jnp.astype(dtype) factorial_l_plus_1 = jnp.exp(jscipy.special.gammaln(l_f64 + 2.0)) factorial_2l_plus_2 = jnp.exp(jscipy.special.gammaln(2.0 * l_f64 + 3.0)) @@ -2955,7 +2998,8 @@ def compute_AOs_grad(aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax.Array Raises: NotImplementedError: If ``aos_data`` is neither Cartesian nor spherical. """ - r_carts = jnp.asarray(r_carts, dtype=jnp.float64) + dtype = get_dtype("kinetic") + r_carts = jnp.asarray(r_carts, dtype=dtype) if isinstance(aos_data, AOs_cart_data): return _compute_AOs_grad_analytic_cart(aos_data, r_carts) @@ -2983,6 +3027,7 @@ def _compute_AOs_grad_autodiff( The dim. of each matrix is (num_ao, N_e) """ + dtype = get_dtype("kinetic") # noqa: F841 grad_full = jacrev(compute_AOs, argnums=1)(aos_data, r_carts) grad_diag = jnp.diagonal(grad_full, axis1=1, axis2=2) grad_diag = jnp.swapaxes(grad_diag, 1, 2) @@ -3004,6 +3049,8 @@ def _compute_AOs_grad_debug( the given atomic orbital at r_carts using FDM for debugging JAX implementations. See compute_AOs_grad_api """ + dtype = get_dtype("kinetic") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 # noqa: F841 # Gradients of AOs (numerical) diff_h = 1.0e-5 diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 60c8b89e..254771f2 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -3,6 +3,11 @@ Module containing classes and methods related to Effective core potential and bare Coulomb potentials +Precision Zones: + - ``coulomb``: all functions in this module. + +See :mod:`jqmc._precision` for details. + Todo: Remove the native 'for' loops for up and down electron positions in the function '_compute_ecp_non_local_parts_NN_jax' and replace them with e.g., jax.lax.scan. @@ -63,6 +68,7 @@ ) from ._function_collections import _legendre_tablated as jnp_legendre_tablated from .jastrow_factor import _compute_ratio_Jastrow_part_split_spin, compute_Jastrow_part +from ._precision import get_dtype from ._setting import NN_default, Nv_default from .structure import ( Structure_data, @@ -606,6 +612,11 @@ def _compute_ecp_local_parts_all_pairs_debug( Returns: float: The sum of local part of the given ECPs with r_up_carts and r_dn_carts. """ + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) + r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) + V_local = 0.0 for i_atom in range(coulomb_potential_data.structure_data.natom): max_ang_mom_plus_1 = coulomb_potential_data.max_ang_mom_plus_1[i_atom] @@ -675,6 +686,12 @@ def _compute_ecp_non_local_parts_all_pairs_debug( list[float]: The list of non-local part of the given ECPs with r_up_carts and r_dn_carts. float: sum of the V_nonlocal """ + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) + r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) + RT = np.asarray(RT, dtype=dtype_np) + if Nv == 4: weights = tetrahedron_sym_mesh_Nv4.weights grid_points = tetrahedron_sym_mesh_Nv4.grid_points @@ -858,6 +875,12 @@ def _compute_ecp_non_local_parts_nearest_neighbors_debug( list[float]: The list of non-local part of the given ECPs with r_up_carts and r_dn_carts. float: sum of the V_nonlocal """ + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) + r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) + RT = np.asarray(RT, dtype=dtype_np) + if Nv == 4: weights = tetrahedron_sym_mesh_Nv4.weights grid_points = tetrahedron_sym_mesh_Nv4.grid_points @@ -1094,6 +1117,12 @@ def _compute_ecp_coulomb_potential_debug( Returns: float: The sum of non-local part of the given ECPs with r_up_carts and r_dn_carts. """ + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) + r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) + RT = np.asarray(RT, dtype=dtype_np) + ecp_local_parts = _compute_ecp_local_parts_all_pairs_debug( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts ) @@ -1180,8 +1209,9 @@ def compute_V_local( ) # Vectrized (flatten) arguments are prepared here. - r_up_carts_jnp = jnp.array(r_up_carts) - r_dn_carts_jnp = jnp.array(r_dn_carts) + dtype = get_dtype("coulomb") + r_up_carts_jnp = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts_jnp = jnp.asarray(r_dn_carts, dtype=dtype) i_atom_np = np.array(coulomb_potential_data._nucleus_index_local_part) exponent_np = np.array(coulomb_potential_data._exponents_local_part) @@ -1243,18 +1273,23 @@ def compute_ecp_non_local_parts_nearest_neighbors( - Non-local ECP contributions per configuration (flattened). - Scalar sum of all non-local contributions. """ + dtype = get_dtype("coulomb") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + RT = jnp.asarray(RT, dtype=dtype) + if Nv == 4: - weights = jnp.array(tetrahedron_sym_mesh_Nv4.weights) - grid_points = jnp.array(tetrahedron_sym_mesh_Nv4.grid_points) + weights = jnp.array(tetrahedron_sym_mesh_Nv4.weights, dtype=dtype) + grid_points = jnp.array(tetrahedron_sym_mesh_Nv4.grid_points, dtype=dtype) elif Nv == 6: - weights = jnp.array(octahedron_sym_mesh_Nv6.weights) - grid_points = jnp.array(octahedron_sym_mesh_Nv6.grid_points) + weights = jnp.array(octahedron_sym_mesh_Nv6.weights, dtype=dtype) + grid_points = jnp.array(octahedron_sym_mesh_Nv6.grid_points, dtype=dtype) elif Nv == 12: - weights = jnp.array(icosahedron_sym_mesh_Nv12.weights) - grid_points = jnp.array(icosahedron_sym_mesh_Nv12.grid_points) + weights = jnp.array(icosahedron_sym_mesh_Nv12.weights, dtype=dtype) + grid_points = jnp.array(icosahedron_sym_mesh_Nv12.grid_points, dtype=dtype) elif Nv == 18: - weights = jnp.array(octahedron_sym_mesh_Nv18.weights) - grid_points = jnp.array(octahedron_sym_mesh_Nv18.grid_points) + weights = jnp.array(octahedron_sym_mesh_Nv18.weights, dtype=dtype) + grid_points = jnp.array(octahedron_sym_mesh_Nv18.grid_points, dtype=dtype) else: raise NotImplementedError @@ -1266,8 +1301,8 @@ def compute_ecp_non_local_parts_nearest_neighbors( global_max_ang_mom_plus_1 = coulomb_potential_data._global_max_ang_mom_plus_1 # stored - non_local_ecp_part_r_carts_up = jnp.zeros((0, len(r_up_carts), 3)) - non_local_ecp_part_r_carts_dn = jnp.zeros((0, len(r_dn_carts), 3)) + non_local_ecp_part_r_carts_up = jnp.zeros((0, len(r_up_carts), 3), dtype=dtype) + non_local_ecp_part_r_carts_dn = jnp.zeros((0, len(r_dn_carts), 3), dtype=dtype) # cos_theta_all = jnp.zeros((0,)) # weight_all = jnp.zeros((0,)) # V_l_mapped_all = jnp.zeros((global_max_ang_mom_plus_1, 0)) @@ -1293,11 +1328,11 @@ def _build_mesh_for_spin(r_carts, other_carts): n_other = other_carts.shape[0] if n_spin == 0: return ( - jnp.zeros((0, n_spin, 3)), - jnp.zeros((0, n_other, 3)), - jnp.zeros((n_spin, 0, global_max_ang_mom_plus_1)), - jnp.zeros((0,)), - jnp.zeros((0,)), + jnp.zeros((0, n_spin, 3), dtype=dtype), + jnp.zeros((0, n_other, 3), dtype=dtype), + jnp.zeros((n_spin, 0, global_max_ang_mom_plus_1), dtype=dtype), + jnp.zeros((0,), dtype=dtype), + jnp.zeros((0,), dtype=dtype), ) i_atom_lists = vmap( @@ -1328,7 +1363,7 @@ def _rels_for_electron(r_cart, i_atom_list): base = r_carts[None, None, None, :, :] r_carts_on_mesh = base + delta_full # (n_spin, NN, Nv, n_spin, 3) if n_other == 0: - other_carts_on_mesh = jnp.zeros((n_spin, NN, grid_points.shape[0], 0, 3)) + other_carts_on_mesh = jnp.zeros((n_spin, NN, grid_points.shape[0], 0, 3), dtype=dtype) else: other_carts_on_mesh = jnp.broadcast_to(other_carts, (n_spin, NN, grid_points.shape[0], n_other, 3)) @@ -1350,7 +1385,7 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): r_mesh = r_carts_on_mesh.reshape(-1, n_spin, 3) if n_other == 0: - other_mesh = jnp.zeros((r_mesh.shape[0], 0, 3)) + other_mesh = jnp.zeros((r_mesh.shape[0], 0, 3), dtype=dtype) else: other_mesh = other_carts_on_mesh.reshape(-1, n_other, 3) return r_mesh, other_mesh, V_l_mapped, cos_theta.reshape(-1), weight.reshape(-1) @@ -1465,18 +1500,23 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( used in the MCMC loop. Passing an inverse from a different configuration silently produces incorrect non-local ECP contributions. """ + dtype = get_dtype("coulomb") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + RT = jnp.asarray(RT, dtype=dtype) + if Nv == 4: - weights = jnp.array(tetrahedron_sym_mesh_Nv4.weights) - grid_points = jnp.array(tetrahedron_sym_mesh_Nv4.grid_points) + weights = jnp.array(tetrahedron_sym_mesh_Nv4.weights, dtype=dtype) + grid_points = jnp.array(tetrahedron_sym_mesh_Nv4.grid_points, dtype=dtype) elif Nv == 6: - weights = jnp.array(octahedron_sym_mesh_Nv6.weights) - grid_points = jnp.array(octahedron_sym_mesh_Nv6.grid_points) + weights = jnp.array(octahedron_sym_mesh_Nv6.weights, dtype=dtype) + grid_points = jnp.array(octahedron_sym_mesh_Nv6.grid_points, dtype=dtype) elif Nv == 12: - weights = jnp.array(icosahedron_sym_mesh_Nv12.weights) - grid_points = jnp.array(icosahedron_sym_mesh_Nv12.grid_points) + weights = jnp.array(icosahedron_sym_mesh_Nv12.weights, dtype=dtype) + grid_points = jnp.array(icosahedron_sym_mesh_Nv12.grid_points, dtype=dtype) elif Nv == 18: - weights = jnp.array(octahedron_sym_mesh_Nv18.weights) - grid_points = jnp.array(octahedron_sym_mesh_Nv18.grid_points) + weights = jnp.array(octahedron_sym_mesh_Nv18.weights, dtype=dtype) + grid_points = jnp.array(octahedron_sym_mesh_Nv18.grid_points, dtype=dtype) else: raise NotImplementedError @@ -1488,8 +1528,8 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( global_max_ang_mom_plus_1 = coulomb_potential_data._global_max_ang_mom_plus_1 # stored - non_local_ecp_part_r_carts_up = jnp.zeros((0, len(r_up_carts), 3)) - non_local_ecp_part_r_carts_dn = jnp.zeros((0, len(r_dn_carts), 3)) + non_local_ecp_part_r_carts_up = jnp.zeros((0, len(r_up_carts), 3), dtype=dtype) + non_local_ecp_part_r_carts_dn = jnp.zeros((0, len(r_dn_carts), 3), dtype=dtype) # cos_theta_all = jnp.zeros((0,)) # weight_all = jnp.zeros((0,)) # V_l_mapped_all = jnp.zeros((global_max_ang_mom_plus_1, 0)) @@ -1515,11 +1555,11 @@ def _build_mesh_for_spin(r_carts, other_carts): n_other = other_carts.shape[0] if n_spin == 0: return ( - jnp.zeros((0, n_spin, 3)), - jnp.zeros((0, n_other, 3)), - jnp.zeros((global_max_ang_mom_plus_1, 0)), - jnp.zeros((0,)), - jnp.zeros((0,)), + jnp.zeros((0, n_spin, 3), dtype=dtype), + jnp.zeros((0, n_other, 3), dtype=dtype), + jnp.zeros((global_max_ang_mom_plus_1, 0), dtype=dtype), + jnp.zeros((0,), dtype=dtype), + jnp.zeros((0,), dtype=dtype), ) i_atom_lists = vmap( @@ -1550,7 +1590,7 @@ def _rels_for_electron(r_cart, i_atom_list): base = r_carts[None, None, None, :, :] r_carts_on_mesh = base + delta_full # (n_spin, NN, Nv, n_spin, 3) if n_other == 0: - other_carts_on_mesh = jnp.zeros((n_spin, NN, grid_points.shape[0], 0, 3)) + other_carts_on_mesh = jnp.zeros((n_spin, NN, grid_points.shape[0], 0, 3), dtype=dtype) else: other_carts_on_mesh = jnp.broadcast_to(other_carts, (n_spin, NN, grid_points.shape[0], n_other, 3)) @@ -1574,7 +1614,7 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): r_mesh = r_carts_on_mesh.reshape(-1, n_spin, 3) if n_other == 0: - other_mesh = jnp.zeros((r_mesh.shape[0], 0, 3)) + other_mesh = jnp.zeros((r_mesh.shape[0], 0, 3), dtype=dtype) else: other_mesh = other_carts_on_mesh.reshape(-1, n_other, 3) return r_mesh, other_mesh, V_l_all, cos_theta.reshape(-1), weight.reshape(-1) @@ -1662,6 +1702,11 @@ def compute_ecp_non_local_parts_all_pairs( - Non-local ECP contributions per configuration (flattened). - Scalar sum of all non-local contributions. """ + dtype = get_dtype("coulomb") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + RT = jnp.asarray(RT, dtype=dtype) + if Nv == 4: weights = tetrahedron_sym_mesh_Nv4.weights grid_points = tetrahedron_sym_mesh_Nv4.grid_points @@ -1793,8 +1838,11 @@ def compute_ecp_non_local_part_all_pairs_jax_weights_grid_points( """ # V_l_cutoff = 1e-5 - weights = jnp.array(weights) - grid_points = jnp.array(grid_points) + dtype = get_dtype("coulomb") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + weights = jnp.array(weights, dtype=dtype) + grid_points = jnp.array(grid_points, dtype=dtype) jastrow_denominator = lax.switch( flag_determinant_only, @@ -1951,9 +1999,9 @@ def compute_V_nonlocal_dn( # Vectrized (flatten) arguments are prepared here. r_up_i_jnp = jnp.arange(len(r_up_carts)) - r_up_carts_jnp = jnp.array(r_up_carts) + r_up_carts_jnp = jnp.asarray(r_up_carts, dtype=dtype) r_dn_i_jnp = jnp.arange(len(r_dn_carts)) - r_dn_carts_jnp = jnp.array(r_dn_carts) + r_dn_carts_jnp = jnp.asarray(r_dn_carts, dtype=dtype) i_atom_np = jnp.array(coulomb_potential_data._nucleus_index_non_local_part) ang_mom_np = jnp.array(coulomb_potential_data._ang_mom_non_local_part) @@ -2010,6 +2058,11 @@ def compute_ecp_coulomb_potential( Returns: float: Sum of local and non-local ECP contributions for the given geometry. """ + dtype = get_dtype("coulomb") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + RT = jnp.asarray(RT, dtype=dtype) + ecp_local_parts = compute_ecp_local_parts_all_pairs( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts ) @@ -2081,6 +2134,11 @@ def compute_ecp_coulomb_potential_fast( :func:`compute_ecp_non_local_parts_nearest_neighbors_fast_update` becomes incorrect and the non-local ratios will be silently wrong. """ + dtype = get_dtype("coulomb") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + RT = jnp.asarray(RT, dtype=dtype) + ecp_local_parts = compute_ecp_local_parts_all_pairs( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts ) @@ -2108,6 +2166,11 @@ def _compute_bare_coulomb_potential_debug( r_dn_carts: npt.NDArray[np.float64], ) -> float: """See compute_bare_coulomb_potential_api.""" + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) + r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) + R_carts = coulomb_potential_data.structure_data._positions_cart_np R_charges = coulomb_potential_data._effective_charges r_up_charges = [-1 for _ in range(len(r_up_carts))] @@ -2142,6 +2205,10 @@ def compute_bare_coulomb_potential( Returns: float: Total bare Coulomb energy. """ + dtype = get_dtype("coulomb") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + interactions_ion_ion = compute_bare_coulomb_potential_ion_ion(coulomb_potential_data) interactions_el_ion_elements_up, interactions_el_ion_elements_dn = compute_bare_coulomb_potential_el_ion_element_wise( coulomb_potential_data, r_up_carts, r_dn_carts @@ -2172,13 +2239,15 @@ def compute_bare_coulomb_potential_el_ion_element_wise( Returns: tuple[jax.Array, jax.Array]: Element-wise ion–electron interactions for up spins and down spins (shape ``(N_up,)`` and ``(N_dn,)``). """ - R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp) + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype) R_charges = np.array(coulomb_potential_data._effective_charges) - r_up_charges = np.full(len(r_up_carts), -1.0, dtype=np.float64) - r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=np.float64) + r_up_charges = np.full(len(r_up_carts), -1.0, dtype=dtype_np) + r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=dtype_np) - r_up_carts = jnp.array(r_up_carts) - r_dn_carts = jnp.array(r_dn_carts) + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) # Define a function to compute interaction for a pair def el_ion_interaction(Z_i, Z_j, r_i, r_j): @@ -2215,13 +2284,15 @@ def compute_discretized_bare_coulomb_potential_el_ion_element_wise( Returns: tuple[jax.Array, jax.Array]: Element-wise ion–electron interactions for up spins and down spins (shape ``(N_up,)`` and ``(N_dn,)``). """ - R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp) + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype) R_charges = np.array(coulomb_potential_data._effective_charges) - r_up_charges = np.full(len(r_up_carts), -1.0, dtype=np.float64) - r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=np.float64) + r_up_charges = np.full(len(r_up_carts), -1.0, dtype=dtype_np) + r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=dtype_np) - r_up_carts = jnp.array(r_up_carts) - r_dn_carts = jnp.array(r_dn_carts) + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) # Define a function to compute interaction for a pair def el_ion_interaction(Z_i, Z_j, r_i, r_j, alat): @@ -2248,6 +2319,11 @@ def _compute_bare_coulomb_potential_el_ion_element_wise_debug( r_dn_carts: npt.NDArray[np.float64], ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """See compute_bare_coulomb_potential_api.""" + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) + r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) + R_carts = coulomb_potential_data.structure_data._positions_cart_np R_charges = coulomb_potential_data._effective_charges r_up_charges = [-1 for _ in range(len(r_up_carts))] @@ -2282,6 +2358,11 @@ def _compute_discretized_bare_coulomb_potential_el_ion_element_wise_debug( alat: float, ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """See compute_bare_coulomb_potential_api.""" + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) + r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) + R_carts = coulomb_potential_data.structure_data._positions_cart_np R_charges = coulomb_potential_data._effective_charges r_up_charges = [-1 for _ in range(len(r_up_carts))] @@ -2323,11 +2404,13 @@ def compute_bare_coulomb_potential_el_el( Returns: float: Electron–electron Coulomb energy. """ - r_up_charges = np.full(len(r_up_carts), -1.0, dtype=np.float64) - r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=np.float64) + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_charges = np.full(len(r_up_carts), -1.0, dtype=dtype_np) + r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=dtype_np) - r_up_carts = jnp.array(r_up_carts) - r_dn_carts = jnp.array(r_dn_carts) + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) all_charges = np.hstack([r_up_charges, r_dn_charges]) all_carts = jnp.vstack([r_up_carts, r_dn_carts]) @@ -2373,7 +2456,8 @@ def compute_bare_coulomb_potential_ion_ion( Returns: float: Ion–ion Coulomb energy. """ - R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp) + dtype = get_dtype("coulomb") + R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype) R_charges = np.array(coulomb_potential_data._effective_charges) all_charges = R_charges @@ -2424,6 +2508,10 @@ def compute_bare_coulomb_potential_el_ion( Returns: float: Electron–ion Coulomb energy. """ + dtype = get_dtype("coulomb") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + interactions_el_ion_elements_up, interactions_el_ion_elements_dn = compute_bare_coulomb_potential_el_ion_element_wise( coulomb_potential_data, r_up_carts, r_dn_carts ) @@ -2441,6 +2529,12 @@ def _compute_coulomb_potential_debug( wavefunction_data: Wavefunction_data = None, ) -> float: """See compute_coulomb_potential_api.""" + dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) + r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) + RT = np.asarray(RT, dtype=dtype_np) + # all-electron if not coulomb_potential_data.ecp_flag: bare_coulomb_potential = _compute_bare_coulomb_potential_debug( @@ -2494,6 +2588,11 @@ def compute_coulomb_potential( Returns: float: Sum of bare Coulomb (ion–ion, electron–ion, electron–electron) and ECP (local + non-local) energies. """ + dtype = get_dtype("coulomb") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + RT = jnp.asarray(RT, dtype=dtype) + # all-electron if not coulomb_potential_data.ecp_flag: bare_coulomb_potential = compute_bare_coulomb_potential( @@ -2559,6 +2658,11 @@ def compute_coulomb_potential_fast( electrons have moved simultaneously the underlying Sherman–Morrison rank-1 update is incorrect and non-local ratios will be silently wrong. """ + dtype = get_dtype("coulomb") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + RT = jnp.asarray(RT, dtype=dtype) + # all-electron — no ECP, no need for A_old_inv if not coulomb_potential_data.ecp_flag: bare_coulomb_potential = compute_bare_coulomb_potential( diff --git a/jqmc/determinant.py b/jqmc/determinant.py index 517cbb6a..5be2211d 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -1,4 +1,12 @@ -"""Determinant module.""" +"""Determinant module. + +Precision Zones: + - ``geminal``: matrix elements (compute_geminal_*). + - ``determinant``: log-det, SVD, antisymmetrisation (compute_ln_det_*, compute_AS_*). + - ``kinetic``: determinant derivatives (compute_grads_and_laplacian_ln_Det*). + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -49,7 +57,8 @@ from flax import struct from jax import jit, vmap -from ._setting import EPS_rcond_SVD, atol_consistency, rtol_consistency +from ._precision import get_dtype +from ._setting import EPS_rcond_SVD, atol_consistency, get_eps, rtol_consistency from .atomic_orbital import ( AOs_cart_data, AOs_sphe_data, @@ -998,13 +1007,10 @@ def compute_ln_det_geminal_all_elements( Returns: float: Scalar log-determinant of the geminal matrix. """ - return jnp.log( - jnp.abs( - jnp.linalg.det( - compute_geminal_all_elements(geminal_data=geminal_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - ) - ) - ) + dtype = get_dtype("determinant") + G = compute_geminal_all_elements(geminal_data=geminal_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) + G = jnp.asarray(G, dtype=dtype) + return jnp.log(jnp.abs(jnp.linalg.det(G))) # Forward pass for custom VJP. @@ -1020,7 +1026,9 @@ def _ln_det_fwd(geminal_data, r_up_carts, r_dn_carts): - primal output: ln|det(G)| - residuals: (inputs and SVD factors) for use in backward pass """ + dtype = get_dtype("determinant") G = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) + G = jnp.asarray(G, dtype=dtype) ln_det = jnp.log(jnp.abs(jnp.linalg.det(G))) # Compute SVD: G = U_svd @ diag(s) @ Vt U_svd, s, Vt = jnp.linalg.svd(G, full_matrices=False) @@ -1051,8 +1059,10 @@ def _ln_det_bwd(res, g): geminal_data, r_up_carts, r_dn_carts, U_svd, s, Vt = res # Compute G^{-1} via SVD pseudoinverse with thresholding. - # Singular values below EPS_rcond_SVD * s_max are zeroed to avoid NaN from 1/~0. - s_inv = jnp.where(s > EPS_rcond_SVD * s[0], 1.0 / s, 0.0) + # Singular values below eps_rcond * s_max are zeroed to avoid NaN from 1/~0. + dtype = get_dtype("determinant") + eps_rcond = get_eps("rcond_svd", dtype) + s_inv = jnp.where(s > eps_rcond * s[0], jnp.asarray(1.0, dtype=dtype) / s, jnp.asarray(0.0, dtype=dtype)) X = (Vt.T * s_inv[jnp.newaxis, :]) @ U_svd.T # G^{-1}, shape (n, n) # d ln|det G| / dG = (G^{-1})^T, scaled by incoming cotangent g @@ -1101,7 +1111,9 @@ def compute_ln_det_geminal_all_elements_fast( used in the MCMC loop. Passing an inverse that corresponds to different electron positions silently produces incorrect gradients. """ + dtype = get_dtype("determinant") G = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) + G = jnp.asarray(G, dtype=dtype) return jnp.log(jnp.abs(jnp.linalg.det(G))) @@ -1111,14 +1123,18 @@ def _ln_det_fast_fwd( r_dn_carts: jax.Array, geminal_inv: jax.Array, ): + dtype = get_dtype("determinant") G = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) + G = jnp.asarray(G, dtype=dtype) val = jnp.log(jnp.abs(jnp.linalg.det(G))) # Save inputs for backward (geminal_inv replaces G^{-1} in bwd) return val, (geminal_data, r_up_carts, r_dn_carts, geminal_inv) def _ln_det_fast_bwd(res, g): + dtype = get_dtype("determinant") geminal_data, r_up_carts, r_dn_carts, geminal_inv = res + geminal_inv = jnp.asarray(geminal_inv, dtype=dtype) # d(ln|det G|)/d(G_{ij}) = (G^{-T})_{ij} # Use the pre-computed inverse instead of re-solving. G_bar = g * geminal_inv.T # cotangent w.r.t. G, shape (N_up, N_up) @@ -1149,7 +1165,10 @@ def compute_det_geminal_all_elements( Returns: float: Scalar determinant of the geminal matrix. """ - return jnp.linalg.det(compute_geminal_all_elements(geminal_data=geminal_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts)) + dtype = get_dtype("determinant") + G = compute_geminal_all_elements(geminal_data=geminal_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) + G = jnp.asarray(G, dtype=dtype) + return jnp.linalg.det(G) def _compute_det_geminal_all_elements_debug( @@ -1158,13 +1177,15 @@ def _compute_det_geminal_all_elements_debug( r_dn_carts: npt.NDArray[np.float64], ) -> np.float64: """See compute_det_geminal_all_elements_api.""" - return np.linalg.det( - _compute_geminal_all_elements_debug( - geminal_data=geminal_data, - r_up_carts=r_up_carts, - r_dn_carts=r_dn_carts, - ) + dtype = get_dtype("determinant") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + G = _compute_geminal_all_elements_debug( + geminal_data=geminal_data, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) + G = np.asarray(G, dtype=dtype_np) + return np.linalg.det(G) def compute_AS_regularization_factor_fast_update( @@ -1179,6 +1200,9 @@ def compute_AS_regularization_factor_fast_update( Returns: jax.Array: Scalar AS regularization factor. """ + dtype = get_dtype("determinant") + geminal = jnp.asarray(geminal, dtype=dtype) + geminal_inv = jnp.asarray(geminal_inv, dtype=dtype) # compute the AS factor theta = 3.0 / 8.0 @@ -1208,7 +1232,10 @@ def _compute_AS_regularization_factor_debug( geminal_data: Geminal_data, r_up_carts: npt.NDArray[np.float64], r_dn_carts: npt.NDArray[np.float64] ) -> npt.NDArray[np.float64]: """See compute_AS_regularization_factor_jax.""" + dtype = get_dtype("determinant") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 geminal = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) + geminal = np.asarray(geminal, dtype=dtype_np) # compute the AS factor theta = 3.0 / 8.0 @@ -1241,15 +1268,20 @@ def compute_AS_regularization_factor(geminal_data: Geminal_data, r_up_carts: jax Returns: jax.Array: Scalar AS regularization factor. """ + dtype = get_dtype("determinant") geminal = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) + geminal = jnp.asarray(geminal, dtype=dtype) # compute the AS factor theta = 3.0 / 8.0 # compute F \equiv the square of Frobenius norm of geminal_inv # Use SVD with conservative threshold to avoid Inf from 1/sigma^2 for tiny sigma + eps_rcond = get_eps("rcond_svd", dtype) sigma = jnp.linalg.svd(geminal, compute_uv=False) - sigma_sq_inv = jnp.where(sigma > EPS_rcond_SVD * sigma[0], 1.0 / (sigma**2), 0.0) + sigma_sq_inv = jnp.where( + sigma > eps_rcond * sigma[0], jnp.asarray(1.0, dtype=dtype) / (sigma**2), jnp.asarray(0.0, dtype=dtype) + ) F = jnp.sum(sigma_sq_inv) # compute the scaling factor @@ -1281,6 +1313,9 @@ def compute_geminal_all_elements(geminal_data: Geminal_data, r_up_carts: jax.Arr Returns: jax.Array: Geminal matrix with shape ``(N_up, N_up)`` combining paired and unpaired blocks. """ + dtype = get_dtype("geminal") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) if len(r_up_carts) != geminal_data.num_electron_up or len(r_dn_carts) != geminal_data.num_electron_dn: logger.info( f"Number of up and dn electrons (N_up, N_dn) = ({len(r_up_carts)}, {len(r_dn_carts)}) are not consistent " @@ -1313,7 +1348,10 @@ def _compute_geminal_all_elements( r_dn_carts: jax.Array, ) -> jax.Array: """See compute_geminal_all_elements_api.""" + dtype = get_dtype("geminal") lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data.lambda_matrix, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) orb_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) orb_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) @@ -1332,7 +1370,13 @@ def _compute_geminal_all_elements_debug( r_dn_carts: npt.NDArray[np.float64], ) -> npt.NDArray[np.float64]: """See compute_geminal_all_elements_api.""" + dtype = get_dtype("geminal") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) + r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) lambda_matrix_paired, lambda_matrix_unpaired = np.hsplit(geminal_data.lambda_matrix, [geminal_data.orb_num_dn]) + lambda_matrix_paired = np.asarray(lambda_matrix_paired, dtype=dtype_np) + lambda_matrix_unpaired = np.asarray(lambda_matrix_unpaired, dtype=dtype_np) orb_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) orb_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) @@ -1361,10 +1405,15 @@ def compute_geminal_up_one_row_elements( Returns: jax.Array: Row vector with shape ``(N_dn + N_unpaired,)``. """ + dtype = get_dtype("geminal") + r_up_cart = jnp.asarray(r_up_cart, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) # Split lambda into paired/unpaired blocks along columns lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit( geminal_data.lambda_matrix, [geminal_data.orb_num_dn] ) # shapes: (n_orb_up, n_orb_dn), (n_orb_up, num_unpaired) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) # Orbital values: # - up: single position -> 1D vector (n_orb_up,) @@ -1403,10 +1452,14 @@ def compute_geminal_dn_one_column_elements( Returns: jax.Array: Column vector for the paired block with shape ``(N_up,)``. """ + dtype = get_dtype("geminal") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_cart = jnp.asarray(r_dn_cart, dtype=dtype) # Split lambda into paired/unpaired blocks along columns lambda_matrix_paired, _lambda_matrix_unpaired = jnp.hsplit( geminal_data.lambda_matrix, [geminal_data.orb_num_dn] ) # lambda_matrix_paired: (n_orb_up, n_orb_dn) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) # Orbital values: # - up: batched positions -> (n_orb_up, N_up) @@ -1463,6 +1516,12 @@ def _compute_ratio_determinant_part_rank1_update( grid generated by the MCMC loop, where exactly one electron is displaced per grid point by construction. """ + dtype = get_dtype("geminal") + A_old_inv = jnp.asarray(A_old_inv, dtype=dtype) + old_r_up_carts = jnp.asarray(old_r_up_carts, dtype=dtype) + old_r_dn_carts = jnp.asarray(old_r_dn_carts, dtype=dtype) + new_r_up_carts_arr = jnp.asarray(new_r_up_carts_arr, dtype=dtype) + new_r_dn_carts_arr = jnp.asarray(new_r_dn_carts_arr, dtype=dtype) num_up = old_r_up_carts.shape[0] num_dn = old_r_dn_carts.shape[0] @@ -1497,6 +1556,8 @@ def _compute_ratio_determinant_part_rank1_update( lambda_matrix_paired, lambda_matrix_unpaired = jnp.split( geminal_data.lambda_matrix, indices_or_sections=[geminal_data.orb_num_dn], axis=1 ) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) # Precompute old AO matrices once. orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts) @@ -1567,6 +1628,12 @@ def _compute_ratio_determinant_part_split_spin( exclusively for the block-structured non-local ECP grids produced by the MCMC loop. """ + dtype = get_dtype("geminal") + A_old_inv = jnp.asarray(A_old_inv, dtype=dtype) + old_r_up_carts = jnp.asarray(old_r_up_carts, dtype=dtype) + old_r_dn_carts = jnp.asarray(old_r_dn_carts, dtype=dtype) + new_r_up_shifted = jnp.asarray(new_r_up_shifted, dtype=dtype) + new_r_dn_shifted = jnp.asarray(new_r_dn_shifted, dtype=dtype) num_up = old_r_up_carts.shape[0] num_dn = old_r_dn_carts.shape[0] @@ -1587,6 +1654,8 @@ def _compute_ratio_determinant_part_split_spin( lambda_matrix_paired, lambda_matrix_unpaired = jnp.split( geminal_data.lambda_matrix, indices_or_sections=[geminal_data.orb_num_dn], axis=1 ) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) # Precompute old AO matrices once. orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts) @@ -1634,6 +1703,12 @@ def _compute_ratio_determinant_part_debug( new_r_dn_carts_arr: npt.NDArray[np.float64], ) -> npt.NDArray: """See _api method.""" + dtype = get_dtype("determinant") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + old_r_up_carts = np.asarray(old_r_up_carts, dtype=dtype_np) + old_r_dn_carts = np.asarray(old_r_dn_carts, dtype=dtype_np) + new_r_up_carts_arr = np.asarray(new_r_up_carts_arr, dtype=dtype_np) + new_r_dn_carts_arr = np.asarray(new_r_dn_carts_arr, dtype=dtype_np) return np.array( [ compute_det_geminal_all_elements(geminal_data, new_r_up_carts, new_r_dn_carts) @@ -1710,18 +1785,25 @@ def compute_grads_and_laplacian_ln_Det( - Laplacians for spin-up electrons with shape ``(N_up,)``. - Laplacians for spin-down electrons with shape ``(N_dn,)``. """ + dtype = get_dtype("kinetic") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) # Compute G_inv via SVD pseudoinverse (numerically stable, avoids LU NaN). G = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) + G = jnp.asarray(G, dtype=dtype) _U, _s, _Vt = jnp.linalg.svd(G, full_matrices=False) # Use conservative threshold to prevent G^{-2} and G^{-3} terms in the # backward pass from diverging. Standard numpy.linalg.pinv uses max(M,N)*eps, # but for de_L/dc (which involves G_inv^2 in the chain rule) we need a larger # safety margin to avoid Inf/NaN in the gradient. EPS_rcond_SVD is set in setting.py # to handle near-singular G while preserving well-conditioned singular values. - _s_inv = jnp.where(_s > EPS_rcond_SVD * _s[0], 1.0 / _s, 0.0) + eps_rcond = get_eps("rcond_svd", dtype) + _s_inv = jnp.where(_s > eps_rcond * _s[0], jnp.asarray(1.0, dtype=dtype) / _s, jnp.asarray(0.0, dtype=dtype)) geminal_inverse = (_Vt.T * _s_inv[jnp.newaxis, :]) @ _U.T lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data.lambda_matrix, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) @@ -1801,7 +1883,13 @@ def _grads_lap_body( passed to ``jax.vjp`` inside the custom VJP backward pass without creating a dependency on the public fast function. """ + dtype = get_dtype("kinetic") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + geminal_inverse = jnp.asarray(geminal_inverse, dtype=dtype) lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data.lambda_matrix, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) @@ -1874,10 +1962,15 @@ def _grads_lap_fwd( r_dn_carts: jax.Array, ): """Forward pass: compute stable G_inv and primal outputs.""" + dtype = get_dtype("kinetic") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) G = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) + G = jnp.asarray(G, dtype=dtype) _U, _s, _Vt = jnp.linalg.svd(G, full_matrices=False) # Use same conservative threshold as in compute_grads_and_laplacian_ln_Det - _s_inv = jnp.where(_s > EPS_rcond_SVD * _s[0], 1.0 / _s, 0.0) + eps_rcond = get_eps("rcond_svd", dtype) + _s_inv = jnp.where(_s > eps_rcond * _s[0], jnp.asarray(1.0, dtype=dtype) / _s, jnp.asarray(0.0, dtype=dtype)) G_inv_stable = (_Vt.T * _s_inv[jnp.newaxis, :]) @ _U.T primals = _grads_lap_body(geminal_data, r_up_carts, r_dn_carts, G_inv_stable) return primals, (geminal_data, r_up_carts, r_dn_carts, G_inv_stable) @@ -1909,7 +2002,9 @@ def _grads_lap_bwd(res, g): :func:`compute_grads_and_laplacian_ln_Det` for details). Keep ``EPS_rcond_SVD`` very small (e.g. ``1e-20``) to avoid this. """ + dtype = get_dtype("kinetic") geminal_data, r_up_carts, r_dn_carts, G_inv_stable = res + G_inv_stable = jnp.asarray(G_inv_stable, dtype=dtype) # Step 1: differentiate _grads_lap_body w.r.t. all args. # This gives direct gradients (AO path) and G_inv_bar (cotangent for G_inv). @@ -1973,7 +2068,14 @@ def compute_grads_and_laplacian_ln_Det_fast( if geminal_inverse is None: raise ValueError("geminal_inverse must be provided for fast evaluation") + dtype = get_dtype("kinetic") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + geminal_inverse = jnp.asarray(geminal_inverse, dtype=dtype) + lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data.lambda_matrix, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) @@ -2046,6 +2148,9 @@ def _compute_grads_and_laplacian_ln_Det_fast_debug( r_dn_carts: jax.Array, ) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]: """Debug helper that uses auto-diff to validate the fast path.""" + dtype = get_dtype("kinetic") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) # Use auto-diff as the reference (independent implementation). grad_ln_D_up, grad_ln_D_dn, lap_ln_D_up, lap_ln_D_dn = _compute_grads_and_laplacian_ln_Det_auto( geminal_data=geminal_data, @@ -2072,6 +2177,9 @@ def _compute_grads_and_laplacian_ln_Det_auto( Uses autodiff on ln|det(G)| to compute gradients w.r.t. electron positions and per-electron Laplacians. """ + dtype = get_dtype("kinetic") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) def ln_det_fn(r_up, r_dn): return compute_ln_det_geminal_all_elements(geminal_data, r_up, r_dn) @@ -2105,6 +2213,10 @@ def _compute_grads_and_laplacian_ln_Det_debug( np.ndarray, ]: """See compute_grads_and_laplacian_ln_Det_api.""" + dtype = get_dtype("kinetic") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) + r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) det_geminal = compute_det_geminal_all_elements( geminal_data=geminal_data, r_up_carts=r_up_carts, diff --git a/jqmc/hamiltonians.py b/jqmc/hamiltonians.py index 0e906dee..ceaccd25 100644 --- a/jqmc/hamiltonians.py +++ b/jqmc/hamiltonians.py @@ -1,4 +1,11 @@ -"""Hamiltonian module.""" +"""Hamiltonian module. + +Precision Zones: + Zone-boundary aggregation -- combines ``kinetic`` (T) and ``coulomb`` (V) + results, cast to ``kinetic`` zone dtype. + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -49,6 +56,7 @@ from jax import jit from jax import typing as jnpt +from ._precision import get_dtype from .coulomb_potential import Coulomb_potential_data, compute_coulomb_potential, compute_coulomb_potential_fast from .structure import Structure_data from .wavefunction import ( @@ -191,6 +199,10 @@ def compute_local_energy( Returns: float: The value of local energy (e_L) with the given wavefunction (float) """ + dtype = get_dtype("kinetic") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + T = compute_kinetic_energy( wavefunction_data=hamiltonian_data.wavefunction_data, r_up_carts=r_up_carts, @@ -205,7 +217,7 @@ def compute_local_energy( wavefunction_data=hamiltonian_data.wavefunction_data, ) - return T + V + return jnp.asarray(T, dtype=dtype) + jnp.asarray(V, dtype=dtype) def compute_local_energy_fast( @@ -251,6 +263,10 @@ def compute_local_energy_fast( Passing an inverse from a different configuration silently produces incorrect kinetic energy. """ + dtype = get_dtype("kinetic") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + T_up_elements, T_dn_elements = compute_kinetic_energy_all_elements_fast_update( wavefunction_data=hamiltonian_data.wavefunction_data, r_up_carts=r_up_carts, @@ -268,7 +284,7 @@ def compute_local_energy_fast( wavefunction_data=hamiltonian_data.wavefunction_data, ) - return T + V + return jnp.asarray(T, dtype=dtype) + jnp.asarray(V, dtype=dtype) @jit @@ -295,6 +311,10 @@ def _compute_local_energy_auto( Returns: float: The value of local energy (e_L) with the given wavefunction (float) """ + dtype = get_dtype("kinetic") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + T = _compute_kinetic_energy_auto( wavefunction_data=hamiltonian_data.wavefunction_data, r_up_carts=r_up_carts, @@ -309,7 +329,7 @@ def _compute_local_energy_auto( wavefunction_data=hamiltonian_data.wavefunction_data, ) - return T + V + return jnp.asarray(T, dtype=dtype) + jnp.asarray(V, dtype=dtype) def _reconstruct_dataclass(cls, obj): @@ -496,7 +516,7 @@ def _load_dataclass_from_hdf5(cls: Type[T], group: h5py.Group) -> T: and "list" not in str(field.type) and "tuple" not in str(field.type) ): - val = jnp.asarray(val, dtype=jnp.float64) + val = jnp.asarray(val, dtype=get_dtype("io")) init_args[field.name] = val elif field.name in group.attrs: diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 56eb9220..ba46d567 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -1,4 +1,12 @@ -"""Jastrow module.""" +"""Jastrow module. + +Precision Zones: + - ``jastrow``: forward Jastrow evaluation (compute_Jastrow_part, J1/J2/J3). + - ``kinetic``: Jastrow derivatives (compute_grads_and_laplacian_Jastrow_*). + - ``mcmc``: ratio and update functions. + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -53,6 +61,7 @@ from jax import typing as jnpt from jax.tree_util import tree_flatten, tree_unflatten +from ._precision import get_dtype from ._setting import EPS_safe_distance, atol_consistency from .atomic_orbital import ( AOs_cart_data, @@ -347,7 +356,7 @@ def _aggregate_nuclear_channel( jnp.ndarray: ``(n_e, hidden_dim)`` messages summarizing nuclear influence. """ if nuclear_embeddings.shape[0] == 0: - return jnp.zeros((radial_features.shape[0], self.hidden_dim)) + return jnp.zeros((radial_features.shape[0], self.hidden_dim), dtype=radial_features.dtype) weights = weights_net(radial_features) messages = weights * nuclear_embeddings[None, :, :] return jnp.sum(messages, axis=1) @@ -424,8 +433,9 @@ def _pairwise_distances(self, A: jnp.ndarray, B: jnp.ndarray) -> jnp.ndarray: jnp.ndarray: ``(n_a, n_b)`` matrix with a small epsilon added before the square root to keep gradients finite when particles coincide. """ + dtype_j = get_dtype("jastrow") if A.shape[0] == 0 or B.shape[0] == 0: - return jnp.zeros((A.shape[0], B.shape[0])) + return jnp.zeros((A.shape[0], B.shape[0]), dtype=dtype_j) diff = A[:, None, :] - B[None, :, :] return jnp.sqrt(jnp.sum(diff**2, axis=-1) + EPS_safe_distance) @@ -439,9 +449,10 @@ def _nuclear_embeddings(self, Z_n: jnp.ndarray) -> jnp.ndarray: jnp.ndarray: ``(n_nuc, hidden_dim)`` embeddings looked up through ``species_lookup``. Returns an empty array when no nuclei are present. """ + dtype = get_dtype("jastrow") n_nuc = Z_n.shape[0] if n_nuc == 0: - return jnp.zeros((0, self.hidden_dim)) + return jnp.zeros((0, self.hidden_dim), dtype=dtype) lookup = jnp.asarray(self.species_lookup) species_ids = jnp.take(lookup, Z_n.astype(jnp.int32), mode="clip") @@ -500,9 +511,10 @@ def __call__( The network is permutation equivariant within each spin channel and rotation invariant by construction of the PhysNet radial features. """ - r_up = jnp.asarray(r_up) - r_dn = jnp.asarray(r_dn) - R_n = jnp.asarray(R_n) + dtype = get_dtype("jastrow") + r_up = jnp.asarray(r_up, dtype=dtype) + r_dn = jnp.asarray(r_dn, dtype=dtype) + R_n = jnp.asarray(R_n, dtype=dtype) Z_n = jnp.asarray(Z_n) n_up = r_up.shape[0] @@ -613,8 +625,10 @@ def _logger_info(self) -> None: @classmethod def init_jastrow_one_body_data(cls, jastrow_1b_param, structure_data, core_electrons, jastrow_1b_type="exp"): """Initialization.""" + dtype = get_dtype("jastrow") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 jastrow_one_body_data = cls( - jastrow_1b_param=np.asarray(jastrow_1b_param, dtype=np.float64).reshape(()), + jastrow_1b_param=np.asarray(jastrow_1b_param, dtype=dtype_np).reshape(()), jastrow_1b_type=jastrow_1b_type, structure_data=structure_data, core_electrons=core_electrons, @@ -641,10 +655,11 @@ def compute_Jastrow_one_body( Returns: float: One-body Jastrow value (before exponentiation). """ + dtype = get_dtype("jastrow") # Retrieve structure data and convert to JAX arrays - R_carts = jnp.array(jastrow_one_body_data.structure_data.positions) - atomic_numbers = jnp.array(jastrow_one_body_data.structure_data.atomic_numbers) - core_electrons = jnp.array(jastrow_one_body_data.core_electrons) + R_carts = jnp.array(jastrow_one_body_data.structure_data.positions, dtype=dtype) + atomic_numbers = jnp.array(jastrow_one_body_data.structure_data.atomic_numbers, dtype=dtype) + core_electrons = jnp.array(jastrow_one_body_data.core_electrons, dtype=dtype) effective_charges = atomic_numbers - core_electrons j1b_type = jastrow_one_body_data.jastrow_1b_type @@ -696,10 +711,12 @@ def _compute_Jastrow_one_body_debug( r_dn_carts: npt.NDArray[np.float64], ) -> float: """See compute_Jastrow_one_body_api.""" + dtype = get_dtype("jastrow") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 positions = jastrow_one_body_data.structure_data.positions atomic_numbers = jastrow_one_body_data.structure_data.atomic_numbers core_electrons = jastrow_one_body_data.core_electrons - effective_charges = np.array(atomic_numbers) - np.array(core_electrons) + effective_charges = np.array(atomic_numbers, dtype=dtype_np) - np.array(core_electrons, dtype=dtype_np) j1b_type = jastrow_one_body_data.jastrow_1b_type @@ -751,9 +768,11 @@ def _compute_grads_and_laplacian_Jastrow_one_body_debug( np.ndarray, ]: """Numerical gradients and Laplacian for one-body Jastrow (debug).""" + dtype = get_dtype("kinetic") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 diff_h = 1.0e-5 - r_up_carts = np.array(r_up_carts, dtype=float) - r_dn_carts = np.array(r_dn_carts, dtype=float) + r_up_carts = np.array(r_up_carts, dtype=dtype_np) + r_dn_carts = np.array(r_dn_carts, dtype=dtype_np) # grad up grad_x_up = [] @@ -817,14 +836,14 @@ def _compute_grads_and_laplacian_Jastrow_one_body_debug( grad_y_dn.append((J_p_y_dn - J_m_y_dn) / (2.0 * diff_h)) grad_z_dn.append((J_p_z_dn - J_m_z_dn) / (2.0 * diff_h)) - grad_J1_up = np.array([grad_x_up, grad_y_up, grad_z_up]).T - grad_J1_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn]).T + grad_J1_up = np.array([grad_x_up, grad_y_up, grad_z_up], dtype=dtype_np).T + grad_J1_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn], dtype=dtype_np).T # laplacian diff_h2 = 1.0e-3 J_ref = _compute_Jastrow_one_body_debug(jastrow_one_body_data, r_up_carts, r_dn_carts) - lap_J1_up = np.zeros(len(r_up_carts), dtype=float) + lap_J1_up = np.zeros(len(r_up_carts), dtype=dtype_np) # laplacians up for r_i, _ in enumerate(r_up_carts): @@ -856,7 +875,7 @@ def _compute_grads_and_laplacian_Jastrow_one_body_debug( lap_J1_up[r_i] = gradgrad_x_up + gradgrad_y_up + gradgrad_z_up - lap_J1_dn = np.zeros(len(r_dn_carts), dtype=float) + lap_J1_dn = np.zeros(len(r_dn_carts), dtype=dtype_np) # laplacians dn for r_i, _ in enumerate(r_dn_carts): @@ -903,8 +922,9 @@ def _compute_grads_and_laplacian_Jastrow_one_body_auto( jax.Array, ]: """Auto-diff gradients and Laplacian for one-body Jastrow.""" - r_up_carts = jnp.array(r_up_carts) - r_dn_carts = jnp.array(r_dn_carts) + dtype = get_dtype("kinetic") + r_up_carts = jnp.array(r_up_carts, dtype=dtype) + r_dn_carts = jnp.array(r_dn_carts, dtype=dtype) grad_J1_up = grad(compute_Jastrow_one_body, argnums=1)(jastrow_one_body_data, r_up_carts, r_dn_carts) grad_J1_dn = grad(compute_Jastrow_one_body, argnums=2)(jastrow_one_body_data, r_up_carts, r_dn_carts) @@ -936,7 +956,8 @@ def compute_grads_and_laplacian_Jastrow_one_body( Gradients for up/down electrons with shapes ``(N_up, 3)`` and ``(N_dn, 3)``, Laplacians for up/down electrons with shapes ``(N_up,)`` and ``(N_dn,)``. """ - positions = jnp.asarray(jastrow_one_body_data.structure_data.positions) + dtype = get_dtype("kinetic") + positions = jnp.asarray(jastrow_one_body_data.structure_data.positions, dtype=dtype) atomic_numbers = jnp.asarray(jastrow_one_body_data.structure_data.atomic_numbers) core_electrons = jnp.asarray(jastrow_one_body_data.core_electrons) z_eff = atomic_numbers - core_electrons @@ -988,8 +1009,8 @@ def _grad_lap_one_spin(r_carts): else: raise ValueError(f"Unknown jastrow_1b_type: {j1b_type}") - grad_up, lap_up = _grad_lap_one_spin(jnp.asarray(r_up_carts)) - grad_dn, lap_dn = _grad_lap_one_spin(jnp.asarray(r_dn_carts)) + grad_up, lap_up = _grad_lap_one_spin(jnp.asarray(r_up_carts, dtype=dtype)) + grad_dn, lap_dn = _grad_lap_one_spin(jnp.asarray(r_dn_carts, dtype=dtype)) return grad_up, grad_dn, lap_up, lap_dn @@ -1056,8 +1077,10 @@ def _logger_info(self) -> None: @classmethod def init_jastrow_two_body_data(cls, jastrow_2b_param=1.0, jastrow_2b_type="pade"): """Initialization.""" + dtype = get_dtype("jastrow") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 jastrow_two_body_data = cls( - jastrow_2b_param=np.asarray(jastrow_2b_param, dtype=np.float64).reshape(()), + jastrow_2b_param=np.asarray(jastrow_2b_param, dtype=dtype_np).reshape(()), jastrow_2b_type=jastrow_2b_type, ) return jastrow_two_body_data @@ -1332,11 +1355,13 @@ def init_jastrow_three_body_data( random_scale: Upper bound of uniform sampler when random_init is True (default 0.01). seed: Optional seed for deterministic initialization when random_init is True. """ + dtype = get_dtype("jastrow") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 if random_init: rng = np.random.default_rng(seed) - j_matrix = rng.uniform(0.0, random_scale, size=(orb_data._num_orb, orb_data._num_orb + 1)) + j_matrix = rng.uniform(0.0, random_scale, size=(orb_data._num_orb, orb_data._num_orb + 1)).astype(dtype_np) else: - j_matrix = np.zeros((orb_data._num_orb, orb_data._num_orb + 1)) + j_matrix = np.zeros((orb_data._num_orb, orb_data._num_orb + 1), dtype=dtype_np) jastrow_three_body_data = cls( orb_data=orb_data, @@ -1359,8 +1384,10 @@ def to_cartesian(self) -> "Jastrow_three_body_data": aos_cart, transform_matrix = _aos_sphe_to_cart(self.orb_data) - square_sph = np.asarray(self.j_matrix[:, :-1], dtype=np.float64) - j1_sph = np.asarray(self.j_matrix[:, -1], dtype=np.float64) + dtype = get_dtype("jastrow") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + square_sph = np.asarray(self.j_matrix[:, :-1], dtype=dtype_np) + j1_sph = np.asarray(self.j_matrix[:, -1], dtype=dtype_np) square_cart = transform_matrix.T @ square_sph @ transform_matrix j1_cart = transform_matrix.T @ j1_sph @@ -1385,8 +1412,10 @@ def to_spherical(self) -> "Jastrow_three_body_data": aos_sphe, transform_pinv = _aos_cart_to_sphe(self.orb_data) - square_cart = np.asarray(self.j_matrix[:, :-1], dtype=np.float64) - j1_cart = np.asarray(self.j_matrix[:, -1], dtype=np.float64) + dtype = get_dtype("jastrow") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + square_cart = np.asarray(self.j_matrix[:, :-1], dtype=dtype_np) + j1_cart = np.asarray(self.j_matrix[:, -1], dtype=dtype_np) square_sph = transform_pinv.T @ square_cart @ transform_pinv j1_sph = transform_pinv.T @ j1_cart @@ -1550,9 +1579,10 @@ def init_from_structure( # Dummy electron positions for parameter initialization: # use one spin-up and one spin-down electron at the origin so that # both PauliNet channels are initialized with valid shapes. - r_up_init = jnp.zeros((1, 3)) - r_dn_init = jnp.zeros((1, 3)) - R_n = jnp.asarray(structure_data.positions) # (n_nuc, 3) + dtype = get_dtype("jastrow") + r_up_init = jnp.zeros((1, 3), dtype=dtype) + r_dn_init = jnp.zeros((1, 3), dtype=dtype) + R_n = jnp.asarray(structure_data.positions, dtype=dtype) # (n_nuc, 3) Z_n = jnp.asarray(structure_data.atomic_numbers) # (n_nuc,) rngs = {"params": key} @@ -1625,14 +1655,15 @@ def compute_Jastrow_three_body( Returns: float: Three-body Jastrow value (before exponentiation). """ + dtype = get_dtype("jastrow") num_electron_up = len(r_up_carts) num_electron_dn = len(r_dn_carts) - aos_up = jnp.array(jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_up_carts)) - aos_dn = jnp.array(jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_dn_carts)) + aos_up = jnp.array(jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_up_carts), dtype=dtype) + aos_dn = jnp.array(jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_dn_carts), dtype=dtype) - K_up = jnp.tril(jnp.ones((num_electron_up, num_electron_up)), k=-1) - K_dn = jnp.tril(jnp.ones((num_electron_dn, num_electron_dn)), k=-1) + K_up = jnp.tril(jnp.ones((num_electron_up, num_electron_up), dtype=dtype), k=-1) + K_dn = jnp.tril(jnp.ones((num_electron_dn, num_electron_dn), dtype=dtype), k=-1) j1_matrix_up = jastrow_three_body_data.j_matrix[:, -1] j1_matrix_dn = jastrow_three_body_data.j_matrix[:, -1] @@ -1640,8 +1671,8 @@ def compute_Jastrow_three_body( j3_matrix_dn_dn = jastrow_three_body_data.j_matrix[:, :-1] j3_matrix_up_dn = jastrow_three_body_data.j_matrix[:, :-1] - e_up = jnp.ones(num_electron_up).T - e_dn = jnp.ones(num_electron_dn).T + e_up = jnp.ones(num_electron_up, dtype=dtype).T + e_dn = jnp.ones(num_electron_dn, dtype=dtype).T # print(f"aos_up.shape={aos_up.shape}") # print(f"aos_dn.shape={aos_dn.shape}") @@ -1668,6 +1699,8 @@ def _compute_Jastrow_three_body_debug( r_dn_carts: npt.NDArray[np.float64], ) -> float: """See _api method.""" + dtype = get_dtype("jastrow") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 aos_up = jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_up_carts) aos_dn = jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_dn_carts) @@ -1852,13 +1885,15 @@ def apply_block_update(self, block: "VariationalParameterBlock") -> "Jastrow_dat in ``Wavefunction_data.get_variational_blocks`` and add the corresponding handling here, without touching the SR/MCMC driver. """ + dtype = get_dtype("jastrow") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 j1 = self.jastrow_one_body_data j2 = self.jastrow_two_body_data j3 = self.jastrow_three_body_data nn3 = self.jastrow_nn_data if block.name == "j1_param" and j1 is not None: - new_param = np.asarray(block.values, dtype=np.float64).reshape(()) + new_param = np.asarray(block.values, dtype=dtype_np).reshape(()) j1 = Jastrow_one_body_data( jastrow_1b_param=new_param, structure_data=j1.structure_data, @@ -1866,26 +1901,26 @@ def apply_block_update(self, block: "VariationalParameterBlock") -> "Jastrow_dat jastrow_1b_type=j1.jastrow_1b_type, ) elif block.name == "j2_param" and j2 is not None: - new_param = np.asarray(block.values, dtype=np.float64).reshape(()) + new_param = np.asarray(block.values, dtype=dtype_np).reshape(()) j2 = Jastrow_two_body_data(jastrow_2b_param=new_param, jastrow_2b_type=j2.jastrow_2b_type) elif block.name == "j3_matrix" and j3 is not None: - j3_new = np.array(block.values) + j3_new = np.array(block.values, dtype=dtype_np) # Symmetrize unconditionally — the method is a no-op for non-symmetric matrices. j3_new = self.symmetrize_j3(j3_new) j3 = Jastrow_three_body_data(orb_data=j3.orb_data, j_matrix=j3_new) elif block.name == "j3_basis_exp" and j3 is not None: - new_exp = np.asarray(block.values, dtype=np.float64) - new_exp = jnp.asarray(self._symmetrize_ao_basis(j3.orb_data, new_exp), dtype=jnp.float64) + new_exp = np.asarray(block.values, dtype=dtype_np) + new_exp = jnp.asarray(self._symmetrize_ao_basis(j3.orb_data, new_exp), dtype=dtype) j3 = j3.with_updated_ao_exponents(new_exp) elif block.name == "j3_basis_coeff" and j3 is not None: - new_coeff = np.asarray(block.values, dtype=np.float64) - new_coeff = jnp.asarray(self._symmetrize_ao_basis(j3.orb_data, new_coeff), dtype=jnp.float64) + new_coeff = np.asarray(block.values, dtype=dtype_np) + new_coeff = jnp.asarray(self._symmetrize_ao_basis(j3.orb_data, new_coeff), dtype=dtype) j3 = j3.with_updated_ao_coefficients(new_coeff) elif block.name == "jastrow_nn_params" and nn3 is not None: # Update NN Jastrow parameters: block.values is the flattened parameter vector. - flat = jnp.asarray(block.values).reshape(-1) + flat = jnp.asarray(block.values, dtype=dtype).reshape(-1) params_new = nn3.unflatten_fn(flat) nn3 = nn3.replace(params=params_new) @@ -1998,8 +2033,9 @@ def compute_Jastrow_part(jastrow_data: Jastrow_data, r_up_carts: jax.Array, r_dn Returns: float: Total Jastrow value before exponentiation. """ - r_up_carts = jnp.asarray(r_up_carts) - r_dn_carts = jnp.asarray(r_dn_carts) + dtype = get_dtype("jastrow") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) J1 = 0.0 J2 = 0.0 @@ -2023,7 +2059,7 @@ def compute_Jastrow_part(jastrow_data: Jastrow_data, r_up_carts: jax.Array, r_dn if nn3.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") - R_n = jnp.asarray(nn3.structure_data.positions) + R_n = jnp.asarray(nn3.structure_data.positions, dtype=dtype) Z_n = jnp.asarray(nn3.structure_data.atomic_numbers) J3_nn = nn3.nn_def.apply({"params": nn3.params}, r_up_carts, r_dn_carts, R_n, Z_n) J3 = J3 + J3_nn @@ -2037,6 +2073,8 @@ def _compute_Jastrow_part_debug( jastrow_data: Jastrow_data, r_up_carts: npt.NDArray[np.float64], r_dn_carts: npt.NDArray[np.float64] ) -> float: """See compute_Jastrow_part_jax for more details.""" + dtype = get_dtype("jastrow") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 J1 = 0.0 J2 = 0.0 J3 = 0.0 @@ -2059,12 +2097,16 @@ def _compute_Jastrow_part_debug( if nn3.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3 (debug).") - R_n = np.asarray(nn3.structure_data.positions, dtype=float) - Z_n = np.asarray(nn3.structure_data.atomic_numbers, dtype=float) + R_n = np.asarray(nn3.structure_data.positions, dtype=dtype_np) + Z_n = np.asarray(nn3.structure_data.atomic_numbers, dtype=dtype_np) # Use JAX NN for debug as well; convert inputs to jnp and back to float J3_nn = nn3.nn_def.apply( - {"params": nn3.params}, jnp.asarray(r_up_carts), jnp.asarray(r_dn_carts), jnp.asarray(R_n), jnp.asarray(Z_n) + {"params": nn3.params}, + jnp.asarray(r_up_carts, dtype=dtype), + jnp.asarray(r_dn_carts, dtype=dtype), + jnp.asarray(R_n, dtype=dtype), + jnp.asarray(Z_n, dtype=dtype), ) J3 += float(J3_nn) @@ -2106,10 +2148,11 @@ def _compute_ratio_Jastrow_part_rank1_update( grid generated by the MCMC loop, where exactly one electron is displaced per grid point by construction. """ - old_r_up_carts = jnp.asarray(old_r_up_carts) - old_r_dn_carts = jnp.asarray(old_r_dn_carts) - new_r_up_carts_arr = jnp.asarray(new_r_up_carts_arr) - new_r_dn_carts_arr = jnp.asarray(new_r_dn_carts_arr) + dtype = get_dtype("mcmc") + old_r_up_carts = jnp.asarray(old_r_up_carts, dtype=dtype) + old_r_dn_carts = jnp.asarray(old_r_dn_carts, dtype=dtype) + new_r_up_carts_arr = jnp.asarray(new_r_up_carts_arr, dtype=dtype) + new_r_dn_carts_arr = jnp.asarray(new_r_dn_carts_arr, dtype=dtype) num_up = old_r_up_carts.shape[0] num_dn = old_r_dn_carts.shape[0] @@ -2119,7 +2162,7 @@ def _compute_ratio_Jastrow_part_rank1_update( jastrow_xp = vmap(compute_Jastrow_part, in_axes=(None, 0, 0))(jastrow_data, new_r_up_carts_arr, new_r_dn_carts_arr) return jnp.exp(jastrow_xp - jastrow_x) - J_ratio = jnp.ones(n_grid) + J_ratio = jnp.ones(n_grid, dtype=dtype) # J1 part if jastrow_data.jastrow_one_body_data is not None: @@ -2133,8 +2176,8 @@ def compute_one_grid_J1(j1_data, new_r_up_carts, new_r_dn_carts, old_r_up_carts, idx_dn = jnp.argmax(nonzero_dn) r_dn_new = new_r_dn_carts[idx_dn] r_dn_old = old_r_dn_carts[idx_dn] - j1_new = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3)), jnp.expand_dims(r_dn_new, axis=0)) - j1_old = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3)), jnp.expand_dims(r_dn_old, axis=0)) + j1_new = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_new, axis=0)) + j1_old = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_old, axis=0)) return jnp.exp(j1_new - j1_old) elif num_dn == 0: @@ -2145,8 +2188,8 @@ def compute_one_grid_J1(j1_data, new_r_up_carts, new_r_dn_carts, old_r_up_carts, idx_up = jnp.argmax(nonzero_up) r_up_new = new_r_up_carts[idx_up] r_up_old = old_r_up_carts[idx_up] - j1_new = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3))) - j1_old = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3))) + j1_new = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3), dtype=dtype)) + j1_old = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3), dtype=dtype)) return jnp.exp(j1_new - j1_old) else: @@ -2165,16 +2208,24 @@ def up_case(args): j1_data, new_r_up_carts, new_r_dn_carts, old_r_up_carts, old_r_dn_carts = args r_up_new = new_r_up_carts[idx_up] r_up_old = old_r_up_carts[idx_up] - j1_new = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3))) - j1_old = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3))) + j1_new = compute_Jastrow_one_body( + j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3), dtype=dtype) + ) + j1_old = compute_Jastrow_one_body( + j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3), dtype=dtype) + ) return jnp.exp(j1_new - j1_old) def dn_case(args): j1_data, new_r_up_carts, new_r_dn_carts, old_r_up_carts, old_r_dn_carts = args r_dn_new = new_r_dn_carts[idx_dn] r_dn_old = old_r_dn_carts[idx_dn] - j1_new = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3)), jnp.expand_dims(r_dn_new, axis=0)) - j1_old = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3)), jnp.expand_dims(r_dn_old, axis=0)) + j1_new = compute_Jastrow_one_body( + j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_new, axis=0) + ) + j1_old = compute_Jastrow_one_body( + j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_old, axis=0) + ) return jnp.exp(j1_new - j1_old) return jax.lax.cond( @@ -2316,7 +2367,7 @@ def _j2_from_dist(dist, param): def compute_pairwise_sums(pos1, pos2): if pos1.shape[0] == 0 or pos2.shape[0] == 0: - return jnp.zeros(pos1.shape[0]) + return jnp.zeros(pos1.shape[0], dtype=dtype) dists = _safe_norm(pos1[:, None, :] - pos2[None, :, :]) vals = _j2_from_dist(dists, j2_param) return jnp.sum(vals, axis=1) @@ -2380,8 +2431,8 @@ def _batch_pairwise_sum(points_a, points_b, param): j1_vec = j3d.j_matrix[:, -1] # (n_ao,) # Old AOs evaluated once - aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts)) # (n_ao, N_up) - aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts)) # (n_ao, N_dn) + aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype) # (n_ao, N_up) + aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts), dtype=dtype) # (n_ao, N_dn) N_batch = new_r_up_carts_arr.shape[0] @@ -2401,8 +2452,8 @@ def _batch_pairwise_sum(points_a, points_b, param): r_old_moved = jnp.where(up_moved_batch[:, None], r_old_up_moved, r_old_dn_moved) # (N, 3) # Single batched AO evaluation for all N configs (replaces N per-config calls inside vmap) - aos_new_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_new_moved)) # (n_ao, N) - aos_old_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_old_moved)) # (n_ao, N) + aos_new_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_new_moved), dtype=dtype) # (n_ao, N) + aos_old_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_old_moved), dtype=dtype) # (n_ao, N) aos_p_batch = aos_new_batch - aos_old_batch # (n_ao, N) # Precompute constant products (independent of config) @@ -2422,8 +2473,8 @@ def _batch_pairwise_sum(points_a, points_b, param): # UP formula ----------------------------------------------------------- V_up = jnp.dot(aos_p_batch.T, W_up) # (N, N_up) P_up = jnp.dot(U_up, aos_p_batch) # (N_up, N) - Q_up_c = (idx_for_Q[:, None] < jnp.arange(num_up)[None, :]).astype(jnp.float64) # (N, N_up) - Q_up_r = (idx_for_Q[:, None] > jnp.arange(num_up)[None, :]).astype(jnp.float64) # (N, N_up) + Q_up_c = (idx_for_Q[:, None] < jnp.arange(num_up)[None, :]).astype(dtype) # (N, N_up) + Q_up_r = (idx_for_Q[:, None] > jnp.arange(num_up)[None, :]).astype(dtype) # (N, N_up) term2_up = jnp.sum(V_up * Q_up_c, axis=1) # (N,) term3_up = jnp.sum(P_up.T * Q_up_r, axis=1) # (N,) term4_up = dn_cross_vec @ aos_p_batch # (N,) @@ -2432,8 +2483,8 @@ def _batch_pairwise_sum(points_a, points_b, param): # DN formula ----------------------------------------------------------- V_dn = jnp.dot(aos_p_batch.T, W_dn) # (N, N_dn) P_dn = jnp.dot(U_dn, aos_p_batch) # (N_dn, N) - Q_dn_c = (idx_for_Q[:, None] < jnp.arange(num_dn)[None, :]).astype(jnp.float64) # (N, N_dn) - Q_dn_r = (idx_for_Q[:, None] > jnp.arange(num_dn)[None, :]).astype(jnp.float64) # (N, N_dn) + Q_dn_c = (idx_for_Q[:, None] < jnp.arange(num_dn)[None, :]).astype(dtype) # (N, N_dn) + Q_dn_r = (idx_for_Q[:, None] > jnp.arange(num_dn)[None, :]).astype(dtype) # (N, N_dn) term2_dn = jnp.sum(V_dn * Q_dn_c, axis=1) # (N,) term3_dn = jnp.sum(P_dn.T * Q_dn_r, axis=1) # (N,) term4_dn = up_cross_vec @ aos_p_batch # (N,) @@ -2455,7 +2506,7 @@ def _batch_pairwise_sum(points_a, points_b, param): if nn.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") - R_n = jnp.asarray(nn.structure_data.positions) + R_n = jnp.asarray(nn.structure_data.positions, dtype=dtype) Z_n = jnp.asarray(nn.structure_data.atomic_numbers) def compute_one_grid_JNN(new_r_up_carts, new_r_dn_carts): @@ -2509,10 +2560,11 @@ def _compute_ratio_Jastrow_part_split_spin( exclusively for the block-structured non-local ECP grids produced by the MCMC loop. """ - old_r_up_carts = jnp.asarray(old_r_up_carts) - old_r_dn_carts = jnp.asarray(old_r_dn_carts) - new_r_up_shifted = jnp.asarray(new_r_up_shifted) - new_r_dn_shifted = jnp.asarray(new_r_dn_shifted) + dtype = get_dtype("mcmc") + old_r_up_carts = jnp.asarray(old_r_up_carts, dtype=dtype) + old_r_dn_carts = jnp.asarray(old_r_dn_carts, dtype=dtype) + new_r_up_shifted = jnp.asarray(new_r_up_shifted, dtype=dtype) + new_r_dn_shifted = jnp.asarray(new_r_dn_shifted, dtype=dtype) num_up = old_r_up_carts.shape[0] num_dn = old_r_dn_carts.shape[0] @@ -2548,8 +2600,8 @@ def _compute_ratio_Jastrow_part_split_spin( r_up_old_moved = old_r_up_carts[idx_up_block] # (G_up, 3) r_dn_old_moved = old_r_dn_carts[idx_dn_block] # (G_dn, 3) - J_up = jnp.ones(g_up) - J_dn = jnp.ones(g_dn) + J_up = jnp.ones(g_up, dtype=dtype) + J_dn = jnp.ones(g_dn, dtype=dtype) # ── J1 part ────────────────────────────────────────────────────────────── if jastrow_data.jastrow_one_body_data is not None: @@ -2557,8 +2609,8 @@ def _compute_ratio_Jastrow_part_split_spin( # UP block: only the moved up electron contributes to the J1 change. def compute_J1_up_one(r_up_new: jax.Array, r_up_old: jax.Array) -> jax.Array: - j1_new = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3))) - j1_old = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3))) + j1_new = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3), dtype=dtype)) + j1_old = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3), dtype=dtype)) return jnp.exp(j1_new - j1_old) J1_up_block = vmap(compute_J1_up_one)(r_up_moved, r_up_old_moved) # (G_up,) @@ -2566,8 +2618,8 @@ def compute_J1_up_one(r_up_new: jax.Array, r_up_old: jax.Array) -> jax.Array: # DN block: only the moved dn electron contributes. def compute_J1_dn_one(r_dn_new: jax.Array, r_dn_old: jax.Array) -> jax.Array: - j1_new = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3)), jnp.expand_dims(r_dn_new, axis=0)) - j1_old = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3)), jnp.expand_dims(r_dn_old, axis=0)) + j1_new = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_new, axis=0)) + j1_old = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_old, axis=0)) return jnp.exp(j1_new - j1_old) J1_dn_block = vmap(compute_J1_dn_one)(r_dn_moved, r_dn_old_moved) # (G_dn,) @@ -2633,8 +2685,8 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: j1_vec = j3d.j_matrix[:, -1] # (n_ao,) # Old AOs evaluated once; column slices give old AO at each moved position. - aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts)) # (n_ao, N_up) - aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts)) # (n_ao, N_dn) + aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype) # (n_ao, N_up) + aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts), dtype=dtype) # (n_ao, N_dn) # Precompute constant products (shared between blocks). W_up = jnp.dot(j3_mat, aos_up_old) # (n_ao, N_up) @@ -2646,15 +2698,15 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: # ── UP BLOCK ───────────────────────────────────────────────────────── # New AOs at the moved up-electron positions; old AOs by column-slice. - aos_up_new_moved = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_up_moved)) # (n_ao, G_up) + aos_up_new_moved = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_up_moved), dtype=dtype) # (n_ao, G_up) aos_up_old_moved = aos_up_old[:, idx_up_block] # (n_ao, G_up) aos_p_up = aos_up_new_moved - aos_up_old_moved # (n_ao, G_up) term1_up = j1_vec @ aos_p_up # (G_up,) V_up_block = jnp.dot(aos_p_up.T, W_up) # (G_up, N_up) P_up_block = jnp.dot(U_up, aos_p_up) # (N_up, G_up) - Q_up_c = (idx_up_block[:, None] < jnp.arange(num_up)[None, :]).astype(jnp.float64) # (G_up, N_up) - Q_up_r = (idx_up_block[:, None] > jnp.arange(num_up)[None, :]).astype(jnp.float64) # (G_up, N_up) + Q_up_c = (idx_up_block[:, None] < jnp.arange(num_up)[None, :]).astype(dtype) # (G_up, N_up) + Q_up_r = (idx_up_block[:, None] > jnp.arange(num_up)[None, :]).astype(dtype) # (G_up, N_up) term2_up = jnp.sum(V_up_block * Q_up_c, axis=1) # (G_up,) term3_up = jnp.sum(P_up_block.T * Q_up_r, axis=1) # (G_up,) term4_up = dn_cross_vec @ aos_p_up # (G_up,) @@ -2662,15 +2714,15 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: # ── DN BLOCK ───────────────────────────────────────────────────────── # New AOs at the moved dn-electron positions; old AOs by column-slice. - aos_dn_new_moved = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_dn_moved)) # (n_ao, G_dn) + aos_dn_new_moved = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_dn_moved), dtype=dtype) # (n_ao, G_dn) aos_dn_old_moved = aos_dn_old[:, idx_dn_block] # (n_ao, G_dn) aos_p_dn = aos_dn_new_moved - aos_dn_old_moved # (n_ao, G_dn) term1_dn = j1_vec @ aos_p_dn # (G_dn,) V_dn_block = jnp.dot(aos_p_dn.T, W_dn) # (G_dn, N_dn) P_dn_block = jnp.dot(U_dn, aos_p_dn) # (N_dn, G_dn) - Q_dn_c = (idx_dn_block[:, None] < jnp.arange(num_dn)[None, :]).astype(jnp.float64) # (G_dn, N_dn) - Q_dn_r = (idx_dn_block[:, None] > jnp.arange(num_dn)[None, :]).astype(jnp.float64) # (G_dn, N_dn) + Q_dn_c = (idx_dn_block[:, None] < jnp.arange(num_dn)[None, :]).astype(dtype) # (G_dn, N_dn) + Q_dn_r = (idx_dn_block[:, None] > jnp.arange(num_dn)[None, :]).astype(dtype) # (G_dn, N_dn) term2_dn = jnp.sum(V_dn_block * Q_dn_c, axis=1) # (G_dn,) term3_dn = jnp.sum(P_dn_block.T * Q_dn_r, axis=1) # (G_dn,) term4_dn = up_cross_vec @ aos_p_dn # (G_dn,) @@ -2682,7 +2734,7 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: if nn.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") - R_n = jnp.asarray(nn.structure_data.positions) + R_n = jnp.asarray(nn.structure_data.positions, dtype=dtype) Z_n = jnp.asarray(nn.structure_data.atomic_numbers) def compute_one_grid_JNN_split(r_up: jax.Array, r_dn: jax.Array) -> jax.Array: @@ -2709,12 +2761,15 @@ def _compute_ratio_Jastrow_part_debug( new_r_dn_carts_arr: npt.NDArray[np.float64], ) -> npt.NDArray: """See _api method.""" + dtype = get_dtype("mcmc") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 return np.array( [ np.exp(compute_Jastrow_part(jastrow_data, new_r_up_carts, new_r_dn_carts)) / np.exp(compute_Jastrow_part(jastrow_data, old_r_up_carts, old_r_dn_carts)) for new_r_up_carts, new_r_dn_carts in zip(new_r_up_carts_arr, new_r_dn_carts_arr, strict=True) - ] + ], + dtype=dtype_np, ) @@ -2745,13 +2800,14 @@ def compute_grads_and_laplacian_Jastrow_part( Gradients for up/down electrons with shapes ``(N_up, 3)`` and ``(N_dn, 3)`` and Laplacians for up/down electrons with shapes ``(N_up,)`` and ``(N_dn,)``. """ - r_up = jnp.asarray(r_up_carts) - r_dn = jnp.asarray(r_dn_carts) + dtype = get_dtype("kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) grad_J_up = jnp.zeros_like(r_up) grad_J_dn = jnp.zeros_like(r_dn) - lap_J_up = jnp.zeros((r_up.shape[0],)) - lap_J_dn = jnp.zeros((r_dn.shape[0],)) + lap_J_up = jnp.zeros((r_up.shape[0],), dtype=dtype) + lap_J_dn = jnp.zeros((r_dn.shape[0],), dtype=dtype) # one-body (analytic) if jastrow_data.jastrow_one_body_data is not None: @@ -2795,9 +2851,9 @@ def compute_grads_and_laplacian_Jastrow_part( if nn3.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") - r_up_carts_jnp = jnp.asarray(r_up_carts) - r_dn_carts_jnp = jnp.asarray(r_dn_carts) - R_n = jnp.asarray(nn3.structure_data.positions) + r_up_carts_jnp = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts_jnp = jnp.asarray(r_dn_carts, dtype=dtype) + R_n = jnp.asarray(nn3.structure_data.positions, dtype=dtype) Z_n = jnp.asarray(nn3.structure_data.atomic_numbers) def _compute_Jastrow_nn_only(r_up, r_dn): @@ -2844,13 +2900,14 @@ def _compute_grads_and_laplacian_Jastrow_part_auto( Returns: the gradients(x,y,z) of J and the sum of laplacians of J at (r_up_carts, r_dn_carts). """ - r_up_carts_jnp = jnp.array(r_up_carts) - r_dn_carts_jnp = jnp.array(r_dn_carts) + dtype = get_dtype("kinetic") + r_up_carts_jnp = jnp.array(r_up_carts, dtype=dtype) + r_dn_carts_jnp = jnp.array(r_dn_carts, dtype=dtype) grad_J_up = jnp.zeros_like(r_up_carts_jnp) grad_J_dn = jnp.zeros_like(r_dn_carts_jnp) - lap_J_up = jnp.zeros((r_up_carts_jnp.shape[0],)) - lap_J_dn = jnp.zeros((r_dn_carts_jnp.shape[0],)) + lap_J_up = jnp.zeros((r_up_carts_jnp.shape[0],), dtype=dtype) + lap_J_dn = jnp.zeros((r_dn_carts_jnp.shape[0],), dtype=dtype) # one-body if jastrow_data.jastrow_one_body_data is not None: @@ -2904,7 +2961,7 @@ def _compute_grads_and_laplacian_Jastrow_part_auto( if nn3.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") - R_n = jnp.asarray(nn3.structure_data.positions) + R_n = jnp.asarray(nn3.structure_data.positions, dtype=dtype) Z_n = jnp.asarray(nn3.structure_data.atomic_numbers) def _compute_Jastrow_nn_only(r_up, r_dn): @@ -2942,10 +2999,12 @@ def _compute_grads_and_laplacian_Jastrow_part_debug( Uses central finite differences to approximate gradients and the sum of Laplacians of J at (r_up_carts, r_dn_carts). """ + dtype = get_dtype("kinetic") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 diff_h = 1.0e-5 - r_up_carts = np.array(r_up_carts, dtype=float) - r_dn_carts = np.array(r_dn_carts, dtype=float) + r_up_carts = np.array(r_up_carts, dtype=dtype_np) + r_dn_carts = np.array(r_dn_carts, dtype=dtype_np) # grad up grad_x_up = [] @@ -3009,14 +3068,14 @@ def _compute_grads_and_laplacian_Jastrow_part_debug( grad_y_dn.append((J_p_y_dn - J_m_y_dn) / (2.0 * diff_h)) grad_z_dn.append((J_p_z_dn - J_m_z_dn) / (2.0 * diff_h)) - grad_J_up = np.array([grad_x_up, grad_y_up, grad_z_up]).T - grad_J_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn]).T + grad_J_up = np.array([grad_x_up, grad_y_up, grad_z_up], dtype=dtype_np).T + grad_J_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn], dtype=dtype_np).T # laplacian diff_h2 = 1.0e-3 J_ref = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - lap_J_up = np.zeros(len(r_up_carts), dtype=float) + lap_J_up = np.zeros(len(r_up_carts), dtype=dtype_np) # laplacians up for r_i, _ in enumerate(r_up_carts): @@ -3048,7 +3107,7 @@ def _compute_grads_and_laplacian_Jastrow_part_debug( lap_J_up[r_i] = gradgrad_x_up + gradgrad_y_up + gradgrad_z_up - lap_J_dn = np.zeros(len(r_dn_carts), dtype=float) + lap_J_dn = np.zeros(len(r_dn_carts), dtype=dtype_np) # laplacians dn for r_i, _ in enumerate(r_dn_carts): @@ -3112,8 +3171,9 @@ def _compute_grads_and_laplacian_Jastrow_two_body_auto( # jastrow_two_body_data, r_up_carts, r_dn_carts # ) # ) - r_up_carts = jnp.array(r_up_carts) - r_dn_carts = jnp.array(r_dn_carts) + dtype = get_dtype("kinetic") + r_up_carts = jnp.array(r_up_carts, dtype=dtype) + r_dn_carts = jnp.array(r_dn_carts, dtype=dtype) # compute grad grad_J2_up = grad(compute_Jastrow_two_body, argnums=1)(jastrow_two_body_data, r_up_carts, r_dn_carts) @@ -3151,19 +3211,20 @@ def compute_grads_and_laplacian_Jastrow_two_body( Gradients for up/down electrons with shapes ``(N_up, 3)`` and ``(N_dn, 3)``, Laplacians for up/down electrons with shapes ``(N_up,)`` and ``(N_dn,)``. """ + dtype = get_dtype("kinetic") a = jastrow_two_body_data.jastrow_2b_param eps = EPS_safe_distance - r_up = jnp.asarray(r_up_carts) - r_dn = jnp.asarray(r_dn_carts) + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) num_up = r_up.shape[0] num_dn = r_dn.shape[0] grad_up = jnp.zeros_like(r_up) grad_dn = jnp.zeros_like(r_dn) - lap_up = jnp.zeros((num_up,)) - lap_dn = jnp.zeros((num_dn,)) + lap_up = jnp.zeros((num_up,), dtype=dtype) + lap_dn = jnp.zeros((num_dn,), dtype=dtype) j2b_type = jastrow_two_body_data.jastrow_2b_type @@ -3239,6 +3300,8 @@ def _compute_grads_and_laplacian_Jastrow_two_body_debug( np.ndarray, ]: """See _api method.""" + dtype = get_dtype("kinetic") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 diff_h = 1.0e-5 # grad up @@ -3351,8 +3414,8 @@ def _compute_grads_and_laplacian_Jastrow_two_body_debug( grad_y_dn.append((J2_p_y_dn - J2_m_y_dn) / (2.0 * diff_h)) grad_z_dn.append((J2_p_z_dn - J2_m_z_dn) / (2.0 * diff_h)) - grad_J2_up = np.array([grad_x_up, grad_y_up, grad_z_up]).T - grad_J2_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn]).T + grad_J2_up = np.array([grad_x_up, grad_y_up, grad_z_up], dtype=dtype_np).T + grad_J2_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn], dtype=dtype_np).T # laplacian diff_h2 = 1.0e-3 # for laplacian @@ -3363,7 +3426,7 @@ def _compute_grads_and_laplacian_Jastrow_two_body_debug( r_dn_carts=r_dn_carts, ) - lap_J2_up = np.zeros(len(r_up_carts), dtype=float) + lap_J2_up = np.zeros(len(r_up_carts), dtype=dtype_np) # laplacians up for r_i, _ in enumerate(r_up_carts): @@ -3420,7 +3483,7 @@ def _compute_grads_and_laplacian_Jastrow_two_body_debug( lap_J2_up[r_i] = gradgrad_x_up + gradgrad_y_up + gradgrad_z_up - lap_J2_dn = np.zeros(len(r_dn_carts), dtype=float) + lap_J2_dn = np.zeros(len(r_dn_carts), dtype=dtype_np) # laplacians dn for r_i, _ in enumerate(r_dn_carts): @@ -3504,6 +3567,10 @@ def _compute_grads_and_laplacian_Jastrow_three_body_auto( Returns: the gradients(x,y,z) of J(threebody) and the sum of laplacians of J(threebody) at (r_up_carts, r_dn_carts). """ + dtype = get_dtype("kinetic") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + # compute grad grad_J3_up = grad(compute_Jastrow_three_body, argnums=1)(jastrow_three_body_data, r_up_carts, r_dn_carts) @@ -3541,6 +3608,7 @@ def compute_grads_and_laplacian_Jastrow_three_body( Gradients for up/down electrons with shapes ``(N_up, 3)`` and ``(N_dn, 3)``, Laplacians for up/down electrons with shapes ``(N_up,)`` and ``(N_dn,)``. """ + dtype = get_dtype("kinetic") orb_data = jastrow_three_body_data.orb_data if isinstance(orb_data, MOs_data): @@ -3554,11 +3622,11 @@ def compute_grads_and_laplacian_Jastrow_three_body( else: raise NotImplementedError - r_up = jnp.asarray(r_up_carts) - r_dn = jnp.asarray(r_dn_carts) + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) - aos_up = jnp.asarray(compute_orb(orb_data, r_up)) # (n_orb, n_up) - aos_dn = jnp.asarray(compute_orb(orb_data, r_dn)) # (n_orb, n_dn) + aos_up = jnp.asarray(compute_orb(orb_data, r_up), dtype=dtype) # (n_orb, n_up) + aos_dn = jnp.asarray(compute_orb(orb_data, r_dn), dtype=dtype) # (n_orb, n_dn) grad_up_x, grad_up_y, grad_up_z = compute_orb_grad(orb_data, r_up) grad_dn_x, grad_dn_y, grad_dn_z = compute_orb_grad(orb_data, r_dn) @@ -3566,34 +3634,34 @@ def compute_grads_and_laplacian_Jastrow_three_body( grad_up = jnp.stack([grad_up_x, grad_up_y, grad_up_z], axis=-1) # (n_orb, n_up, 3) grad_dn = jnp.stack([grad_dn_x, grad_dn_y, grad_dn_z], axis=-1) # (n_orb, n_dn, 3) - lap_up = jnp.asarray(compute_orb_lapl(orb_data, r_up)) # (n_orb, n_up) - lap_dn = jnp.asarray(compute_orb_lapl(orb_data, r_dn)) # (n_orb, n_dn) + lap_up = jnp.asarray(compute_orb_lapl(orb_data, r_up), dtype=dtype) # (n_orb, n_up) + lap_dn = jnp.asarray(compute_orb_lapl(orb_data, r_dn), dtype=dtype) # (n_orb, n_dn) - j1_vec = jnp.asarray(jastrow_three_body_data.j_matrix[:, -1]) # (n_orb,) - j3_mat = jnp.asarray(jastrow_three_body_data.j_matrix[:, :-1]) # (n_orb, n_orb) + j1_vec = jnp.asarray(jastrow_three_body_data.j_matrix[:, -1], dtype=dtype) # (n_orb,) + j3_mat = jnp.asarray(jastrow_three_body_data.j_matrix[:, :-1], dtype=dtype) # (n_orb, n_orb) num_up = aos_up.shape[1] num_dn = aos_dn.shape[1] # Precompute pair-accumulation masks - upper_up = jnp.triu(jnp.ones((num_up, num_up)), k=1) - lower_up = jnp.tril(jnp.ones((num_up, num_up)), k=-1) - upper_dn = jnp.triu(jnp.ones((num_dn, num_dn)), k=1) - lower_dn = jnp.tril(jnp.ones((num_dn, num_dn)), k=-1) + upper_up = jnp.triu(jnp.ones((num_up, num_up), dtype=dtype), k=1) + lower_up = jnp.tril(jnp.ones((num_up, num_up), dtype=dtype), k=-1) + upper_dn = jnp.triu(jnp.ones((num_dn, num_dn), dtype=dtype), k=1) + lower_dn = jnp.tril(jnp.ones((num_dn, num_dn), dtype=dtype), k=-1) # dJ/dA for each electron (orbital-space coefficients) g_up = ( j1_vec[:, None] + jnp.dot(j3_mat, aos_up) @ lower_up + jnp.dot(j3_mat.T, aos_up) @ upper_up - + jnp.dot(j3_mat, aos_dn) @ jnp.ones((num_dn, 1)) + + jnp.dot(j3_mat, aos_dn) @ jnp.ones((num_dn, 1), dtype=dtype) ) # (n_orb, n_up) g_dn = ( j1_vec[:, None] + jnp.dot(j3_mat, aos_dn) @ lower_dn + jnp.dot(j3_mat.T, aos_dn) @ upper_dn - + jnp.dot(j3_mat.T, aos_up) @ jnp.ones((num_up, 1)) + + jnp.dot(j3_mat.T, aos_up) @ jnp.ones((num_up, 1), dtype=dtype) ) # (n_orb, n_dn) grad_J3_up = jnp.einsum("on,onj->nj", g_up, grad_up) @@ -3616,6 +3684,8 @@ def _compute_grads_and_laplacian_Jastrow_three_body_debug( np.ndarray, ]: """See _api method.""" + dtype = get_dtype("kinetic") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 diff_h = 1.0e-5 # grad up @@ -3728,8 +3798,8 @@ def _compute_grads_and_laplacian_Jastrow_three_body_debug( grad_y_dn.append((J3_p_y_dn - J3_m_y_dn) / (2.0 * diff_h)) grad_z_dn.append((J3_p_z_dn - J3_m_z_dn) / (2.0 * diff_h)) - grad_J3_up = np.array([grad_x_up, grad_y_up, grad_z_up]).T - grad_J3_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn]).T + grad_J3_up = np.array([grad_x_up, grad_y_up, grad_z_up], dtype=dtype_np).T + grad_J3_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn], dtype=dtype_np).T # laplacian diff_h2 = 1.0e-3 # for laplacian @@ -3740,7 +3810,7 @@ def _compute_grads_and_laplacian_Jastrow_three_body_debug( r_dn_carts=r_dn_carts, ) - lap_J3_up = np.zeros(len(r_up_carts), dtype=float) + lap_J3_up = np.zeros(len(r_up_carts), dtype=dtype_np) # laplacians up for r_i, _ in enumerate(r_up_carts): @@ -3797,7 +3867,7 @@ def _compute_grads_and_laplacian_Jastrow_three_body_debug( lap_J3_up[r_i] = gradgrad_x_up + gradgrad_y_up + gradgrad_z_up - lap_J3_dn = np.zeros(len(r_dn_carts), dtype=float) + lap_J3_dn = np.zeros(len(r_dn_carts), dtype=dtype_np) # laplacians dn for r_i, _ in enumerate(r_dn_carts): diff --git a/jqmc/jqmc_cli.py b/jqmc/jqmc_cli.py index af1c6ebe..c790b506 100644 --- a/jqmc/jqmc_cli.py +++ b/jqmc/jqmc_cli.py @@ -48,6 +48,7 @@ # jQMC from ._header_footer import _print_footer, _print_header +from ._precision import configure as configure_precision from ._setting import ( GFMC_MIN_BIN_BLOCKS, GFMC_MIN_COLLECT_STEPS, @@ -242,6 +243,13 @@ def _cli(): logger.info(f" {key}={item}") logger.info("") + # --- precision configuration --- + precision_config = dict_toml.get("precision", {}) + if not isinstance(precision_config, dict): + raise ValueError("The [precision] section must be a TOML table.") + configure_precision(precision_config) + logger.info("") + # default parameters parameters = cli_parameters.copy() diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index b010a213..0bc30685 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -1,4 +1,10 @@ -"""QMC module.""" +"""QMC module (GFMC). + +Precision Zones: + - ``gfmc``: all GFMC propagation functions. + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -51,6 +57,7 @@ from ._diff_mask import DiffMask, apply_diff_mask from ._jqmc_utility import _generate_init_electron_configurations +from ._precision import get_dtype from ._setting import ( GFMC_MIN_BIN_BLOCKS, GFMC_MIN_COLLECT_STEPS, @@ -58,7 +65,7 @@ GFMC_ON_THE_FLY_BIN_BLOCKS, GFMC_ON_THE_FLY_COLLECT_STEPS, GFMC_ON_THE_FLY_WARMUP_STEPS, - EPS_rcond_SVD, + get_eps, rtol_debug_vs_production, ) from .coulomb_potential import ( @@ -273,8 +280,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - self.__latest_r_up_carts = jnp.array(r_carts_up) - self.__latest_r_dn_carts = jnp.array(r_carts_dn) + dtype = get_dtype("gfmc") + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -311,22 +319,26 @@ def __init_attributes(self): n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn n_atoms = self.__hamiltonian_data.structure_data.natom + # gfmc zone dtype for stored numpy arrays + dtype = get_dtype("gfmc") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + # stored weight (w_L) - self.__stored_w_L = np.zeros((0, 1)) + self.__stored_w_L = np.zeros((0, 1), dtype=dtype_np) # stored local energy (e_L) - self.__stored_e_L = np.zeros((0, 1)) + self.__stored_e_L = np.zeros((0, 1), dtype=dtype_np) # stored local energy (e_L2) - self.__stored_e_L2 = np.zeros((0, 1)) + self.__stored_e_L2 = np.zeros((0, 1), dtype=dtype_np) # average projection counter - self.__stored_average_projection_counter = np.zeros((0,)) + self.__stored_average_projection_counter = np.zeros((0,), dtype=dtype_np) # stored force products (per-walker cross-correlation preserved) - self.__stored_force_HF = np.zeros((0, 1, n_atoms, 3)) - self.__stored_force_PP = np.zeros((0, 1, n_atoms, 3)) - self.__stored_E_L_force_PP = np.zeros((0, 1, n_atoms, 3)) + self.__stored_force_HF = np.zeros((0, 1, n_atoms, 3), dtype=dtype_np) + self.__stored_force_PP = np.zeros((0, 1, n_atoms, 3), dtype=dtype_np) + self.__stored_E_L_force_PP = np.zeros((0, 1, n_atoms, 3), dtype=dtype_np) def __validate_stored_shapes(self): """Assert that all stored observable arrays have consistent shapes.""" @@ -484,8 +496,9 @@ def load_from_hdf5(cls, filepath: str, rank: int | None = None) -> "GFMC_t": obj._GFMC_t__jax_PRNG_key_list_init = jnp.array(rng["jax_PRNG_key_list_init"]) # -- Walker state -- - obj._GFMC_t__latest_r_up_carts = jnp.array(ws["latest_r_up_carts"]) - obj._GFMC_t__latest_r_dn_carts = jnp.array(ws["latest_r_dn_carts"]) + dtype = get_dtype("gfmc") + obj._GFMC_t__latest_r_up_carts = jnp.asarray(ws["latest_r_up_carts"], dtype=dtype) + obj._GFMC_t__latest_r_dn_carts = jnp.asarray(ws["latest_r_dn_carts"], dtype=dtype) # -- Observables -- def _load_obs(obs_arr, default): @@ -660,6 +673,9 @@ def run(self, num_mcmc_steps: int = 50, max_time: int = 86400) -> None: np.random.seed(self.__mpi_seed) # precompute geminal inverses per walker for fast kinetic updates + dtype = get_dtype("gfmc") + eps_rcond = get_eps("rcond_svd", dtype) + def _compute_initial_A_inv_t(r_up_carts, r_dn_carts): geminal = compute_geminal_all_elements( geminal_data=self.__hamiltonian_data.wavefunction_data.geminal_data, @@ -667,7 +683,7 @@ def _compute_initial_A_inv_t(r_up_carts, r_dn_carts): r_dn_carts=r_dn_carts, ) U, s, Vt = jnp.linalg.svd(geminal, full_matrices=False) - s_inv = jnp.where(s > EPS_rcond_SVD * s[0], 1.0 / s, 0.0) + s_inv = jnp.where(s > eps_rcond * s[0], 1.0 / s, 0.0) return (Vt.T * s_inv[jnp.newaxis, :]) @ U.T self.__latest_A_old_inv = vmap(_compute_initial_A_inv_t, in_axes=(0, 0))( @@ -1089,8 +1105,8 @@ def _update_inv_dn_t(_): logger.info("Start compilation of the GFMC projection funciton.") logger.info(" Compilation is in progress...") projection_counter_list = jnp.array([0 for _ in range(self.__num_walkers)]) - tau_left_list = jnp.array([self.__tau for _ in range(self.__num_walkers)]) - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)]) + tau_left_list = jnp.array([self.__tau for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) (_, _, _, _, _, _, _, _, _) = vmap(_projection_t, in_axes=(0, 0, 0, 0, 0, 0, 0, None, None, None, None))( projection_counter_list, tau_left_list, @@ -1335,7 +1351,7 @@ def _compute_local_energy_t( start_init_force = time.perf_counter() logger.info("Start compilation of force gradient functions.") logger.info(" Compilation is in progress...") - _dummy_RTs = jnp.stack([jnp.eye(3)] * self.__num_walkers) + _dummy_RTs = jnp.stack([jnp.eye(3, dtype=get_dtype("gfmc"))] * self.__num_walkers) _, _, _ = _jit_vmap_grad_e_L_t( hamiltonian_for_position_grads, self.__latest_r_up_carts, @@ -1367,9 +1383,12 @@ def _compute_local_energy_t( num_mcmc_done = 0 # -- Extend stored arrays with zero-padding for new steps -- + # gfmc zone dtype for stored numpy arrays + dtype_gfmc = get_dtype("gfmc") + dtype_np = np.float64 if dtype_gfmc == jnp.float64 else np.float32 # average_projection_counter is stored on all ranks self.__stored_average_projection_counter = np.concatenate( - [self.__stored_average_projection_counter, np.zeros((num_mcmc_steps,))] + [self.__stored_average_projection_counter, np.zeros((num_mcmc_steps,), dtype=dtype_np)] ) # other observables are stored on rank 0 only if mpi_rank == 0: @@ -1377,14 +1396,18 @@ def _compute_local_energy_t( n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn n_atoms = self.__hamiltonian_data.structure_data.natom - self.__stored_e_L = np.concatenate([self.__stored_e_L, np.zeros((num_mcmc_steps, 1))]) - self.__stored_e_L2 = np.concatenate([self.__stored_e_L2, np.zeros((num_mcmc_steps, 1))]) - self.__stored_w_L = np.concatenate([self.__stored_w_L, np.zeros((num_mcmc_steps, 1))]) + self.__stored_e_L = np.concatenate([self.__stored_e_L, np.zeros((num_mcmc_steps, 1), dtype=dtype_np)]) + self.__stored_e_L2 = np.concatenate([self.__stored_e_L2, np.zeros((num_mcmc_steps, 1), dtype=dtype_np)]) + self.__stored_w_L = np.concatenate([self.__stored_w_L, np.zeros((num_mcmc_steps, 1), dtype=dtype_np)]) if self.__comput_position_deriv: - self.__stored_force_HF = np.concatenate([self.__stored_force_HF, np.zeros((num_mcmc_steps, 1, n_atoms, 3))]) - self.__stored_force_PP = np.concatenate([self.__stored_force_PP, np.zeros((num_mcmc_steps, 1, n_atoms, 3))]) + self.__stored_force_HF = np.concatenate( + [self.__stored_force_HF, np.zeros((num_mcmc_steps, 1, n_atoms, 3), dtype=dtype_np)] + ) + self.__stored_force_PP = np.concatenate( + [self.__stored_force_PP, np.zeros((num_mcmc_steps, 1, n_atoms, 3), dtype=dtype_np)] + ) self.__stored_E_L_force_PP = np.concatenate( - [self.__stored_E_L_force_PP, np.zeros((num_mcmc_steps, 1, n_atoms, 3))] + [self.__stored_E_L_force_PP, np.zeros((num_mcmc_steps, 1, n_atoms, 3), dtype=dtype_np)] ) for i_branching in range(num_mcmc_steps): @@ -1397,8 +1420,8 @@ def _compute_local_energy_t( # Always set the initial weight list to 1.0 projection_counter_list = jnp.array([0 for _ in range(self.__num_walkers)]) - tau_left_list = jnp.array([self.__tau for _ in range(self.__num_walkers)]) - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)]) + tau_left_list = jnp.array([self.__tau for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) start_projection = time.perf_counter() # projection loop @@ -1681,6 +1704,9 @@ def _compute_local_energy_t( local_probabilities = w_L_latest / global_weight_sum # Compute the local cumulative probabilities. + # NOTE: MPI reductions for branching probabilities are kept float64 + # unconditionally (regardless of the gfmc precision zone) to avoid + # population collapse from float32 round-off in the branching step. local_cumprob = np.cumsum(local_probabilities) local_sum_arr = np.array(np.sum(local_probabilities), dtype=np.float64) offset_arr = np.zeros(1, dtype=np.float64) @@ -2637,8 +2663,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - self.__latest_r_up_carts = jnp.array(r_carts_up) - self.__latest_r_dn_carts = jnp.array(r_carts_dn) + dtype = get_dtype("gfmc") + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -2993,7 +3020,7 @@ def _compute_local_energy_t_debug( # Always set the initial weight list to 1.0 projection_counter_list = jnp.array([0 for _ in range(self.__num_walkers)]) e_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)]) - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)]) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) logger.devel(" Projection is on going....") @@ -3036,7 +3063,7 @@ def _compute_local_energy_t_debug( # generate a random rotation matrix jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) - R = jnp.eye(3) # Rotate in the order x -> y -> z + R = jnp.eye(3, dtype=get_dtype("gfmc")) # Rotate in the order x -> y -> z # compute discretized kinetic energy and mesh (with a random rotation) mesh_kinetic_part_r_up_carts, mesh_kinetic_part_r_dn_carts, elements_non_diagonal_kinetic_part = ( @@ -3311,7 +3338,7 @@ def _compute_local_energy_t_debug( # atomic force related if self.__comput_position_deriv: # RT is always eye(3) in _GFMC_t_debug (no random_discretized_mesh) - RT_eye = jnp.eye(3) + RT_eye = jnp.eye(3, dtype=get_dtype("gfmc")) _grad_e_L_fn = grad(_compute_local_energy_t_debug, argnums=(0, 1, 2)) _grad_e_L_results = [ @@ -3944,8 +3971,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - self.__latest_r_up_carts = jnp.array(r_carts_up) - self.__latest_r_dn_carts = jnp.array(r_carts_dn) + dtype = get_dtype("gfmc") + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -3982,19 +4010,23 @@ def __init_attributes(self): n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn n_atoms = self.__hamiltonian_data.structure_data.natom + # gfmc zone dtype for stored numpy arrays + dtype = get_dtype("gfmc") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + # stored weight (w_L) - self.__stored_w_L = np.zeros((0, 1)) + self.__stored_w_L = np.zeros((0, 1), dtype=dtype_np) # stored local energy (e_L) - self.__stored_e_L = np.zeros((0, 1)) + self.__stored_e_L = np.zeros((0, 1), dtype=dtype_np) # stored local energy (e_L2) - self.__stored_e_L2 = np.zeros((0, 1)) + self.__stored_e_L2 = np.zeros((0, 1), dtype=dtype_np) # stored force products (per-walker cross-correlation preserved) - self.__stored_force_HF = np.zeros((0, 1, n_atoms, 3)) - self.__stored_force_PP = np.zeros((0, 1, n_atoms, 3)) - self.__stored_E_L_force_PP = np.zeros((0, 1, n_atoms, 3)) + self.__stored_force_HF = np.zeros((0, 1, n_atoms, 3), dtype=dtype_np) + self.__stored_force_PP = np.zeros((0, 1, n_atoms, 3), dtype=dtype_np) + self.__stored_E_L_force_PP = np.zeros((0, 1, n_atoms, 3), dtype=dtype_np) # stored G_L and G_e_L for updating the E_scf (kept as lists — variable count per run) self.__G_L = [] @@ -4157,8 +4189,9 @@ def load_from_hdf5(cls, filepath: str, rank: int | None = None) -> "GFMC_n": obj._GFMC_n__jax_PRNG_key_list_init = jnp.array(rng["jax_PRNG_key_list_init"]) # -- Walker state -- - obj._GFMC_n__latest_r_up_carts = jnp.array(ws["latest_r_up_carts"]) - obj._GFMC_n__latest_r_dn_carts = jnp.array(ws["latest_r_dn_carts"]) + dtype = get_dtype("gfmc") + obj._GFMC_n__latest_r_up_carts = jnp.asarray(ws["latest_r_up_carts"], dtype=dtype) + obj._GFMC_n__latest_r_dn_carts = jnp.asarray(ws["latest_r_dn_carts"], dtype=dtype) # -- Observables -- n_up = obj._GFMC_n__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up @@ -4339,6 +4372,9 @@ def run(self, num_mcmc_steps: int = 50, max_time: int = 86400) -> None: gfmc_total_start = time.perf_counter() # precompute geminal inverses per walker for fast updates across projections + dtype = get_dtype("gfmc") + eps_rcond = get_eps("rcond_svd", dtype) + def _compute_initial_A_inv_n(r_up_carts, r_dn_carts): geminal = compute_geminal_all_elements( geminal_data=self.__hamiltonian_data.wavefunction_data.geminal_data, @@ -4346,7 +4382,7 @@ def _compute_initial_A_inv_n(r_up_carts, r_dn_carts): r_dn_carts=r_dn_carts, ) U, s, Vt = jnp.linalg.svd(geminal, full_matrices=False) - s_inv = jnp.where(s > EPS_rcond_SVD * s[0], 1.0 / s, 0.0) + s_inv = jnp.where(s > eps_rcond * s[0], 1.0 / s, 0.0) return (Vt.T * s_inv[jnp.newaxis, :]) @ U.T _jit_vmap_A_inv_n = jit(vmap(_compute_initial_A_inv_n, in_axes=(0, 0))) @@ -4799,10 +4835,10 @@ def _split_body(current_key, _): init_w_L, init_r_up_carts, init_r_dn_carts, - jnp.eye(3), + jnp.eye(3, dtype=get_dtype("gfmc")), init_A_old_inv, - jnp.asarray(0.0), - jnp.asarray(0.0), + jnp.asarray(0.0, dtype=get_dtype("gfmc")), + jnp.asarray(0.0, dtype=get_dtype("gfmc")), ), ) @@ -4843,13 +4879,14 @@ def _compute_V_elements_n( if use_fast_update: # precompute geminal inverse for fast updates (SVD-based, robust for near-singular G) + _eps_rcond = get_eps("rcond_svd", get_dtype("gfmc")) geminal = compute_geminal_all_elements( geminal_data=hamiltonian_data.wavefunction_data.geminal_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts, ) _U, _s, _Vt = jnp.linalg.svd(geminal, full_matrices=False) - _s_inv = jnp.where(_s > EPS_rcond_SVD * _s[0], 1.0 / _s, 0.0) + _s_inv = jnp.where(_s > _eps_rcond * _s[0], 1.0 / _s, 0.0) A_old_inv = (_Vt.T * _s_inv[jnp.newaxis, :]) @ _U.T # compute discretized kinetic energy and mesh (with a random rotation) @@ -5133,7 +5170,7 @@ def _compute_local_energy_n( start_init = time.perf_counter() logger.info("Start compilation of the GFMC projection funciton.") logger.info(" Compilation is in progress...") - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)]) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) ( _, _, @@ -5199,14 +5236,22 @@ def _compute_local_energy_n( n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn n_atoms = self.__hamiltonian_data.structure_data.natom - self.__stored_e_L = np.concatenate([self.__stored_e_L, np.zeros((num_mcmc_steps, 1))]) - self.__stored_e_L2 = np.concatenate([self.__stored_e_L2, np.zeros((num_mcmc_steps, 1))]) - self.__stored_w_L = np.concatenate([self.__stored_w_L, np.zeros((num_mcmc_steps, 1))]) + # gfmc zone dtype for stored numpy arrays + dtype_gfmc = get_dtype("gfmc") + dtype_np = np.float64 if dtype_gfmc == jnp.float64 else np.float32 + + self.__stored_e_L = np.concatenate([self.__stored_e_L, np.zeros((num_mcmc_steps, 1), dtype=dtype_np)]) + self.__stored_e_L2 = np.concatenate([self.__stored_e_L2, np.zeros((num_mcmc_steps, 1), dtype=dtype_np)]) + self.__stored_w_L = np.concatenate([self.__stored_w_L, np.zeros((num_mcmc_steps, 1), dtype=dtype_np)]) if self.__comput_position_deriv: - self.__stored_force_HF = np.concatenate([self.__stored_force_HF, np.zeros((num_mcmc_steps, 1, n_atoms, 3))]) - self.__stored_force_PP = np.concatenate([self.__stored_force_PP, np.zeros((num_mcmc_steps, 1, n_atoms, 3))]) + self.__stored_force_HF = np.concatenate( + [self.__stored_force_HF, np.zeros((num_mcmc_steps, 1, n_atoms, 3), dtype=dtype_np)] + ) + self.__stored_force_PP = np.concatenate( + [self.__stored_force_PP, np.zeros((num_mcmc_steps, 1, n_atoms, 3), dtype=dtype_np)] + ) self.__stored_E_L_force_PP = np.concatenate( - [self.__stored_E_L_force_PP, np.zeros((num_mcmc_steps, 1, n_atoms, 3))] + [self.__stored_E_L_force_PP, np.zeros((num_mcmc_steps, 1, n_atoms, 3), dtype=dtype_np)] ) progress = (self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 @@ -5225,7 +5270,7 @@ def _compute_local_energy_n( ) # Always set the initial weight list to 1.0 - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)]) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) start_projection = time.perf_counter() @@ -5526,6 +5571,9 @@ def _compute_local_energy_n( local_probabilities = w_L_latest / global_weight_sum # Compute the local cumulative probabilities. + # NOTE: MPI reductions for branching probabilities are kept float64 + # unconditionally (regardless of the gfmc precision zone) to avoid + # population collapse from float32 round-off in the branching step. local_cumprob = np.cumsum(local_probabilities) local_sum_arr = np.array(np.sum(local_probabilities), dtype=np.float64) offset_arr = np.zeros(1, dtype=np.float64) @@ -6542,8 +6590,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - self.__latest_r_up_carts = jnp.array(r_carts_up) - self.__latest_r_dn_carts = jnp.array(r_carts_dn) + dtype = get_dtype("gfmc") + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -7241,7 +7290,7 @@ def _compute_local_energy_n_debug( ) # Always set the initial weight list to 1.0 - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)]) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) logger.devel(" Projection is on going....") diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index c1e541f8..53890a61 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -1,4 +1,11 @@ -"""QMC module.""" +"""QMC module (VMC / MCMC). + +Precision Zones: + - ``mcmc``: sampling, Sherman--Morrison updates, accept/reject, statistics. + - ``optimization``: SR matrix construction and parameter updates (run_optimize). + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -53,12 +60,14 @@ from ._diff_mask import DiffMask, apply_diff_mask from ._jqmc_utility import _generate_init_electron_configurations +from ._precision import get_dtype from ._setting import ( MCMC_MIN_BIN_BLOCKS, MCMC_MIN_WARMUP_STEPS, EPS_rcond_SVD, EPS_zero_division, atol_consistency, + get_eps, min_S_diag_abs, ) from .atomic_orbital import compute_overlap_matrix @@ -224,8 +233,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - self.__latest_r_up_carts = jnp.array(r_carts_up) - self.__latest_r_dn_carts = jnp.array(r_carts_dn) + dtype = get_dtype("mcmc") + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -254,23 +264,27 @@ def __init_attributes(self): nw = self.__num_walkers n_atoms = self.__hamiltonian_data.structure_data.natom + # mcmc zone dtype for stored numpy arrays + dtype = get_dtype("mcmc") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + # stored weight (w_L) - self.__stored_w_L = np.zeros((0, nw)) + self.__stored_w_L = np.zeros((0, nw), dtype=dtype_np) # stored local energy (e_L) - self.__stored_e_L = np.zeros((0, nw)) + self.__stored_e_L = np.zeros((0, nw), dtype=dtype_np) # stored local energy (e_L2) - self.__stored_e_L2 = np.zeros((0, nw)) + self.__stored_e_L2 = np.zeros((0, nw), dtype=dtype_np) # stored force_HF per walker (HF force = de_L/dR + Omega . de_L/dr) - self.__stored_force_HF = np.zeros((0, nw, n_atoms, 3)) + self.__stored_force_HF = np.zeros((0, nw, n_atoms, 3), dtype=dtype_np) # stored force_PP per walker (Pulay force = dln_Psi/dR + Omega . dln_Psi/dr + 1/2 * d_omega/dr) - self.__stored_force_PP = np.zeros((0, nw, n_atoms, 3)) + self.__stored_force_PP = np.zeros((0, nw, n_atoms, 3), dtype=dtype_np) # stored E_L * force_PP per walker (for covariance in Pulay force) - self.__stored_E_L_force_PP = np.zeros((0, nw, n_atoms, 3)) + self.__stored_E_L_force_PP = np.zeros((0, nw, n_atoms, 3), dtype=dtype_np) # stored parameter gradients keyed by block name self.__stored_log_WF_param_grads: dict[str, list] = defaultdict(list) @@ -486,7 +500,10 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__latest_r_dn_carts, ) - RTs = jnp.broadcast_to(jnp.eye(3), (len(self.__jax_PRNG_key_list), 3, 3)) + dtype = get_dtype("mcmc") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + + RTs = jnp.broadcast_to(jnp.eye(3, dtype=dtype), (len(self.__jax_PRNG_key_list), 3, 3)) # Warm-up compilation: trigger JIT tracing on the first run() call # so that the MCMC loop does not stall on the first step. @@ -620,14 +637,18 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: nw = self.__num_walkers n_atoms = self.__hamiltonian_data.structure_data.natom - self.__stored_e_L = np.concatenate([self.__stored_e_L, np.zeros((num_mcmc_steps, nw))]) - self.__stored_e_L2 = np.concatenate([self.__stored_e_L2, np.zeros((num_mcmc_steps, nw))]) - self.__stored_w_L = np.concatenate([self.__stored_w_L, np.zeros((num_mcmc_steps, nw))]) + self.__stored_e_L = np.concatenate([self.__stored_e_L, np.zeros((num_mcmc_steps, nw), dtype=dtype_np)]) + self.__stored_e_L2 = np.concatenate([self.__stored_e_L2, np.zeros((num_mcmc_steps, nw), dtype=dtype_np)]) + self.__stored_w_L = np.concatenate([self.__stored_w_L, np.zeros((num_mcmc_steps, nw), dtype=dtype_np)]) if self.__comput_position_deriv: - self.__stored_force_HF = np.concatenate([self.__stored_force_HF, np.zeros((num_mcmc_steps, nw, n_atoms, 3))]) - self.__stored_force_PP = np.concatenate([self.__stored_force_PP, np.zeros((num_mcmc_steps, nw, n_atoms, 3))]) + self.__stored_force_HF = np.concatenate( + [self.__stored_force_HF, np.zeros((num_mcmc_steps, nw, n_atoms, 3), dtype=dtype_np)] + ) + self.__stored_force_PP = np.concatenate( + [self.__stored_force_PP, np.zeros((num_mcmc_steps, nw, n_atoms, 3), dtype=dtype_np)] + ) self.__stored_E_L_force_PP = np.concatenate( - [self.__stored_E_L_force_PP, np.zeros((num_mcmc_steps, nw, n_atoms, 3))] + [self.__stored_E_L_force_PP, np.zeros((num_mcmc_steps, nw, n_atoms, 3), dtype=dtype_np)] ) geminal, geminal_inv, _, _ = _geminal_inv_batched( @@ -699,7 +720,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: if self.__random_discretized_mesh: RTs = _jit_vmap_generate_RTs(self.__jax_PRNG_key_list) else: - RTs = jnp.broadcast_to(jnp.eye(3), (len(self.__jax_PRNG_key_list), 3, 3)) + RTs = jnp.broadcast_to(jnp.eye(3, dtype=dtype), (len(self.__jax_PRNG_key_list), 3, 3)) # Evaluate observables each MCMC cycle start = time.perf_counter() @@ -787,10 +808,10 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: else: n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn - omega_up_step = jnp.zeros((nw, n_atoms, n_up)) - omega_dn_step = jnp.zeros((nw, n_atoms, n_dn)) - grad_omega_dr_up_step = jnp.zeros((nw, n_atoms, 3)) - grad_omega_dr_dn_step = jnp.zeros((nw, n_atoms, 3)) + omega_up_step = jnp.zeros((nw, n_atoms, n_up), dtype=dtype) + omega_dn_step = jnp.zeros((nw, n_atoms, n_dn), dtype=dtype) + grad_omega_dr_up_step = jnp.zeros((nw, n_atoms, 3), dtype=dtype) + grad_omega_dr_dn_step = jnp.zeros((nw, n_atoms, 3), dtype=dtype) # Compute per-walker force products preserving cross-correlations _grad_e_L_r_up_np = np.array(grad_e_L_r_up_step) # (nw, n_up, 3) @@ -2190,10 +2211,12 @@ def solve_linear_method( # ================================================================== # ---- Step 1: Remove parameters with near-zero diag(S) ---- + dtype_opt = get_dtype("optimization") + dtype_opt_np = np.float64 if dtype_opt == jnp.float64 else np.float32 diag_S = np.diag(S_matrix) max_diag_S = np.max(np.abs(diag_S)) # parcut2 ~ machine_precision^2, effectively only removes exact zeros - parcut2 = np.finfo(np.float64).eps ** 2 + parcut2 = np.finfo(dtype_opt_np).eps ** 2 alive = np.abs(diag_S) > parcut2 * max_diag_S n_removed_step1 = p - int(np.count_nonzero(alive)) if n_removed_step1 > 0: @@ -2201,11 +2224,11 @@ def solve_linear_method( if not np.any(alive): logger.warning(" LM dgelscut: all parameters removed in Step 1; returning zero update.") - return np.zeros(p), H_0 + return np.zeros(p, dtype=dtype_opt_np), H_0 # ---- Step 2: Build correlation matrix for alive parameters ---- alive_idx = np.where(alive)[0] - D_inv_sqrt = np.zeros(p) + D_inv_sqrt = np.zeros(p, dtype=dtype_opt_np) D_inv_sqrt[alive_idx] = 1.0 / np.sqrt(np.abs(diag_S[alive_idx])) # ---- Step 3: Iteratively remove parameters until well-conditioned ---- @@ -2214,7 +2237,7 @@ def solve_linear_method( n_alive = len(idx) if n_alive == 0: logger.warning(" LM dgelscut: all parameters removed; returning zero update.") - return np.zeros(p), H_0 + return np.zeros(p, dtype=dtype_opt_np), H_0 # Build correlation matrix for current alive set D_sub = D_inv_sqrt[idx] # (n_alive,) @@ -2274,7 +2297,7 @@ def solve_linear_method( if p_prime == 0: logger.warning(" LM: no positive S eigenvalues after dgelscut; returning zero update.") - return np.zeros(p), H_0 + return np.zeros(p, dtype=dtype_opt_np), H_0 # P = U Λ^{-1/2} (S-orthonormal basis) inv_sqrt_Lambda = 1.0 / np.sqrt(Lambda) @@ -2286,8 +2309,8 @@ def solve_linear_method( # ---- Build extended matrices (p'+1) x (p'+1) ---- dim = p_prime + 1 - H_bar = np.zeros((dim, dim)) - S_bar = np.eye(dim) # identity (S-orthonormal basis) + H_bar = np.zeros((dim, dim), dtype=dtype_opt_np) + S_bar = np.eye(dim, dtype=dtype_opt_np) # identity (S-orthonormal basis) H_bar[0, 0] = H_0 H_bar[0, 1:] = -0.5 * f_new @@ -2324,7 +2347,7 @@ def solve_linear_method( # ---- Back-transform: P @ c_new → alive parameter space → full space ---- c_alive = P @ c_new # (n_alive,) - c_vec = np.zeros(p) + c_vec = np.zeros(p, dtype=dtype_opt_np) c_vec[idx] = c_alive logger.info( @@ -2540,9 +2563,11 @@ def _conjugate_gradient_numpy( max_iter: int, tol: float, ) -> tuple[npt.NDArray[np.float64], float, int]: - x = np.array(x0, dtype=np.float64, copy=True) - r = np.array(b, dtype=np.float64, copy=False) - apply_A(x) - p = np.array(r, dtype=np.float64, copy=True) + dtype_opt = get_dtype("optimization") + dtype_opt_np = np.float64 if dtype_opt == jnp.float64 else np.float32 + x = np.array(x0, dtype=dtype_opt_np, copy=True) + r = np.array(b, dtype=dtype_opt_np, copy=False) - apply_A(x) + p = np.array(r, dtype=dtype_opt_np, copy=True) rs_old = float(np.dot(r, r)) if not np.isfinite(rs_old): @@ -2551,7 +2576,7 @@ def _conjugate_gradient_numpy( if np.sqrt(rs_old) <= tol: return x, np.sqrt(rs_old), 0 - tiny = np.finfo(np.float64).tiny + tiny = np.finfo(dtype_opt_np).tiny num_iter = 0 for i in range(int(max_iter)): Ap = apply_A(p) @@ -2677,6 +2702,9 @@ def _conjugate_gradient_numpy( logger.info(f"Bin blocks = {num_mcmc_bin_blocks}.") logger.info("") + dtype_opt = get_dtype("optimization") + dtype_opt_np = np.float64 if dtype_opt == jnp.float64 else np.float32 + lambda_projectors = None num_orb_projection = None if opt_with_projected_MOs: @@ -2684,10 +2712,14 @@ def _conjugate_gradient_numpy( geminal_mo_current = wavefunction_data_step.geminal_data num_orb_projection = int(geminal_mo_current.num_electron_dn) - mo_coefficients_up = np.asarray(geminal_mo_current.orb_data_up_spin.mo_coefficients, dtype=np.float64) - mo_coefficients_dn = np.asarray(geminal_mo_current.orb_data_dn_spin.mo_coefficients, dtype=np.float64) - overlap_up = np.asarray(compute_overlap_matrix(geminal_mo_current.orb_data_up_spin.aos_data), dtype=np.float64) - overlap_dn = np.asarray(compute_overlap_matrix(geminal_mo_current.orb_data_dn_spin.aos_data), dtype=np.float64) + mo_coefficients_up = np.asarray(geminal_mo_current.orb_data_up_spin.mo_coefficients, dtype=dtype_opt_np) + mo_coefficients_dn = np.asarray(geminal_mo_current.orb_data_dn_spin.mo_coefficients, dtype=dtype_opt_np) + overlap_up = np.asarray( + compute_overlap_matrix(geminal_mo_current.orb_data_up_spin.aos_data), dtype=dtype_opt_np + ) + overlap_dn = np.asarray( + compute_overlap_matrix(geminal_mo_current.orb_data_dn_spin.aos_data), dtype=dtype_opt_np + ) overlap_up = 0.5 * (overlap_up + overlap_up.T) overlap_dn = 0.5 * (overlap_dn + overlap_dn.T) @@ -2723,7 +2755,7 @@ def _conjugate_gradient_numpy( # ------------------------------------------------------------------ # DEVEL: orthogonal complement-projector diagnostics (I - L') and (I - R') # ------------------------------------------------------------------ - _I = np.eye(left_projector.shape[0], dtype=np.float64) + _I = np.eye(left_projector.shape[0], dtype=dtype_opt_np) _comp_L = _I - left_projector # (I - L') — symmetric _comp_R = _I - right_projector # (I - R') — symmetric @@ -2843,13 +2875,15 @@ def _conjugate_gradient_numpy( if not (use_sr or use_lm): if blocks: - flat_param_vector = np.concatenate([np.ravel(np.array(block.values, dtype=np.float64)) for block in blocks]) + flat_param_vector = np.concatenate( + [np.ravel(np.array(block.values, dtype=dtype_opt_np)) for block in blocks] + ) else: - flat_param_vector = np.array([], dtype=np.float64) + flat_param_vector = np.array([], dtype=dtype_opt_np) if optax_state is None: optax_param_size = flat_param_vector.size - optax_state = optax_tx.init(jnp.array(flat_param_vector)) + optax_state = optax_tx.init(jnp.asarray(flat_param_vector, dtype=dtype_opt)) elif flat_param_vector.size != optax_param_size: raise ValueError("The number of variational parameters changed after initializing the optax optimizer.") @@ -3004,7 +3038,7 @@ def _conjugate_gradient_numpy( # compute X_w@F X_F_local = X_local @ F_local # shape (num_param, ) - X_F = np.empty(X_F_local.shape, dtype=np.float64) + X_F = np.empty(X_F_local.shape, dtype=dtype_opt_np) mpi_comm.Allreduce(X_F_local, X_F, op=MPI.SUM) # compute f_argmax (index in reduced space) @@ -3026,7 +3060,7 @@ def _conjugate_gradient_numpy( # make the SR matrix scale-invariant (i.e., normalize) ## compute X_w@X.T diag_S_local = np.einsum("jk,kj->j", X_local, X_local.T) - diag_S = np.empty(diag_S_local.shape, dtype=np.float64) + diag_S = np.empty(diag_S_local.shape, dtype=dtype_opt_np) mpi_comm.Allreduce(diag_S_local, diag_S, op=MPI.SUM) logger.info(f"max. and min. diag_S = {np.max(diag_S)}, {np.min(diag_S)}.") # ------------------------------------------------------------------ @@ -3087,7 +3121,7 @@ def _conjugate_gradient_numpy( logger.devel(f"X_X_T_local.shape = {X_X_T_local.shape}.") # compute global sum of X * X^T if mpi_rank == 0: - X_X_T = np.empty(X_X_T_local.shape, dtype=np.float64) + X_X_T = np.empty(X_X_T_local.shape, dtype=dtype_opt_np) else: X_X_T = None mpi_comm.Reduce(X_X_T_local, X_X_T, op=MPI.SUM, root=0) @@ -3096,7 +3130,7 @@ def _conjugate_gradient_numpy( logger.devel(f"X_F_local.shape = {X_F_local.shape}.") # compute global sum of X @ F if mpi_rank == 0: - X_F = np.empty(X_F_local.shape, dtype=np.float64) + X_F = np.empty(X_F_local.shape, dtype=dtype_opt_np) else: X_F = None mpi_comm.Reduce(X_F_local, X_F, op=MPI.SUM, root=0) @@ -3141,9 +3175,9 @@ def apply_S_primal_numpy(v): x0 = np.zeros_like(X_F) theta_all, final_residual, num_steps = _conjugate_gradient_numpy( - np.asarray(X_F, dtype=np.float64), + np.asarray(X_F, dtype=dtype_opt_np), apply_S_primal_numpy, - np.asarray(x0, dtype=np.float64), + np.asarray(x0, dtype=dtype_opt_np), sr_cg_max_iter, sr_cg_tol, ) @@ -3213,7 +3247,7 @@ def apply_S_primal_numpy(v): logger.devel(f"X_T_X_local.shape = {X_T_X_local.shape}.") # compute global sum of X^T * X if mpi_rank == 0: - X_T_X = np.empty(X_T_X_local.shape, dtype=np.float64) + X_T_X = np.empty(X_T_X_local.shape, dtype=dtype_opt_np) else: X_T_X = None mpi_comm.Reduce(X_T_X_local, X_T_X, op=MPI.SUM, root=0) @@ -3222,7 +3256,7 @@ def apply_S_primal_numpy(v): F_recvcounts = mpi_comm.gather(F_local_count, root=0) if mpi_rank == 0: F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] - F = np.empty(sum(F_recvcounts), dtype=np.float64) + F = np.empty(sum(F_recvcounts), dtype=dtype_opt_np) else: F_displs = None F = None @@ -3244,7 +3278,7 @@ def apply_S_primal_numpy(v): # Broadcast K to all ranks so they know how big each chunk is K = mpi_comm.bcast(K, root=0) - X_T_X_inv_F_local = np.empty(K, dtype=np.float64) + X_T_X_inv_F_local = np.empty(K, dtype=dtype_opt_np) mpi_comm.Scatter( [X_T_X_inv_F, MPI.DOUBLE], # send buffer (only significant on root) @@ -3253,7 +3287,7 @@ def apply_S_primal_numpy(v): ) # theta = X_w (X^T X_w + eps*I)^{-1} F theta_all_local = X_local @ X_T_X_inv_F_local - theta_all = np.empty(theta_all_local.shape, dtype=np.float64) + theta_all = np.empty(theta_all_local.shape, dtype=dtype_opt_np) mpi_comm.Allreduce(theta_all_local, theta_all, op=MPI.SUM) logger.devel(f"[new] theta_all (w/ the push through identity) = {theta_all}.") logger.devel( @@ -3275,7 +3309,7 @@ def apply_dual_S_numpy(v): F_local_count = F_local.shape[0] F_recvcounts = mpi_comm.allgather(F_local_count) F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] - F_total = np.empty(sum(F_recvcounts), dtype=np.float64) + F_total = np.empty(sum(F_recvcounts), dtype=dtype_opt_np) mpi_comm.Allgatherv( [F_local, MPI.DOUBLE], [F_total, (F_recvcounts, F_displs), MPI.DOUBLE], @@ -3287,7 +3321,7 @@ def apply_dual_S_numpy(v): x_sol, final_residual, num_steps = _conjugate_gradient_numpy( F_total, apply_dual_S_numpy, - np.asarray(x0, dtype=np.float64), + np.asarray(x0, dtype=dtype_opt_np), sr_cg_max_iter, sr_cg_tol, ) @@ -3378,10 +3412,10 @@ def apply_dual_S_numpy(v): # optax optimizer ############################# else: - params = jnp.array(flat_param_vector) - grads = -jnp.array(f) + params = jnp.asarray(flat_param_vector, dtype=dtype_opt) + grads = -jnp.asarray(f, dtype=dtype_opt) updates, optax_state = optax_tx.update(grads, optax_state, params) - theta_all = np.array(updates, dtype=np.float64) + theta_all = np.array(updates, dtype=dtype_opt_np) if optax_param_size is None: optax_param_size = flat_param_vector.size self.__set_optimizer_runtime( @@ -3398,11 +3432,11 @@ def apply_dual_S_numpy(v): # 1) Expand theta_all to full parameter space. # ------------------------------------------------------------------ if use_sr: - theta = np.zeros(total_num_params, dtype=np.float64) + theta = np.zeros(total_num_params, dtype=dtype_opt_np) theta[:] = theta_all else: # optax - theta = np.zeros(total_num_params, dtype=np.float64) + theta = np.zeros(total_num_params, dtype=dtype_opt_np) theta[:] = theta_all # ------------------------------------------------------------------ @@ -3485,7 +3519,7 @@ def apply_dual_S_numpy(v): theta = 0.1 * g_sr else: # Back-transform: c_vec[0] = c₀ (SR direction), c_vec[1:] = c_k (individual params) - theta = np.zeros(total_num_params, dtype=np.float64) + theta = np.zeros(total_num_params, dtype=dtype_opt_np) theta[:] += c_vec[0] * g_sr # SR collective variable (affects all params) if lm_subspace_dim == -1 or lm_subspace_dim >= total_num_params: theta[:] += c_vec[1:] @@ -3532,7 +3566,7 @@ def apply_dual_S_numpy(v): # ------------------------------------------------------------------ if use_sr and lambda_projectors is not None and len(lambda_projectors) == 4: _left_proj, _right_proj, _, _ = lambda_projectors - _identity_proj = np.eye(_left_proj.shape[0], dtype=np.float64) + _identity_proj = np.eye(_left_proj.shape[0], dtype=dtype_opt_np) _comp_L = _identity_proj - _left_proj _comp_R = _identity_proj - _right_proj for _blk, _s, _e in offsets: @@ -4190,6 +4224,7 @@ def comput_e_L_param_deriv(self) -> bool: @jit def _generate_rotation_matrix(jax_PRNG_key): """Sample a random 3×3 rotation matrix (Euler angles).""" + dtype = get_dtype("mcmc") _, subkey = jax.random.split(jax_PRNG_key) alpha, beta, gamma = jax.random.uniform(subkey, shape=(3,), minval=-2 * jnp.pi, maxval=2 * jnp.pi) cos_a, sin_a = jnp.cos(alpha), jnp.sin(alpha) @@ -4200,7 +4235,8 @@ def _generate_rotation_matrix(jax_PRNG_key): [cos_b * cos_g, cos_g * sin_a * sin_b - cos_a * sin_g, sin_a * sin_g + cos_a * cos_g * sin_b], [cos_b * sin_g, cos_a * cos_g + sin_a * sin_b * sin_g, cos_a * sin_b * sin_g - cos_g * sin_a], [-sin_b, cos_b * sin_a, cos_a * cos_b], - ] + ], + dtype=dtype, ) return R.T @@ -4208,13 +4244,15 @@ def _generate_rotation_matrix(jax_PRNG_key): @jit def _geminal_inv_single(geminal_data, I, r_up_carts, r_dn_carts): """Build G and invert via SVD-based pseudoinverse (single sample).""" + dtype = get_dtype("mcmc") + eps_rcond = get_eps("rcond_svd", dtype) G = compute_geminal_all_elements( geminal_data=geminal_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts, ) U, s, Vt = jnp.linalg.svd(G, full_matrices=False) - s_inv = jnp.where(s > EPS_rcond_SVD * s[0], 1.0 / s, 0.0) + s_inv = jnp.where(s > eps_rcond * s[0], 1.0 / s, 0.0) Ginv = (Vt.T * s_inv[jnp.newaxis, :]) @ U.T return G, Ginv, jnp.zeros_like(G), jnp.zeros(G.shape[0], dtype=jnp.int32) @@ -4222,8 +4260,9 @@ def _geminal_inv_single(geminal_data, I, r_up_carts, r_dn_carts): @jit def _geminal_inv_batched(geminal_data, r_up_batch, r_dn_batch): """Batched geminal inverse over walkers.""" + dtype = get_dtype("mcmc") N_up = r_up_batch.shape[-2] - I = jnp.eye(N_up) + I = jnp.eye(N_up, dtype=dtype) G_b, Ginv_b, lu_b, piv_b = vmap( _geminal_inv_single, in_axes=(None, None, 0, 0), @@ -4262,10 +4301,11 @@ def _update_electron_positions( updated_r_up_cart (jnpt.ArrayLike): up electron position. dim: (N_e^up, 3) updated_r_dn_cart (jnpt.ArrayLike): down electron position. dim: (N_e^down, 3) """ + dtype = get_dtype("mcmc") accepted_moves = 0 rejected_moves = 0 - r_up_carts = init_r_up_carts - r_dn_carts = init_r_dn_carts + r_up_carts = jnp.asarray(init_r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(init_r_dn_carts, dtype=dtype) geminal = geminal_init geminal_inv = geminal_inv_init @@ -4323,7 +4363,7 @@ def body_fun(_, carry): random_index = jax.random.randint(subkey, shape=(), minval=0, maxval=3) # plug g into g_vector - g_vector = jnp.zeros(3) + g_vector = jnp.zeros(3, dtype=dtype) g_vector = g_vector.at[random_index].set(g) new_r_cart = old_r_cart + g_vector @@ -4434,7 +4474,7 @@ def body_fun(_, carry): # compute R_ratio R_ratio = (R_AS_ratio * WF_ratio) ** 2.0 - acceptance_ratio = jnp.min(jnp.array([1.0, R_ratio * T_ratio])) + acceptance_ratio = jnp.min(jnp.array([1.0, R_ratio * T_ratio], dtype=dtype)) jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) b = jax.random.uniform(subkey, shape=(), minval=0.0, maxval=1.0) @@ -4486,10 +4526,11 @@ def _update_electron_positions_only_up_electron( geminal_init, ): """Update electron positions based on the MH method (up-spin electrons only).""" + dtype = get_dtype("mcmc") accepted_moves = 0 rejected_moves = 0 - r_up_carts = init_r_up_carts - r_dn_carts = init_r_dn_carts + r_up_carts = jnp.asarray(init_r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(init_r_dn_carts, dtype=dtype) geminal_inv = geminal_inv_init geminal = geminal_init @@ -4539,7 +4580,7 @@ def body_fun(_, carry): random_index = jax.random.randint(subkey, shape=(), minval=0, maxval=3) # plug g into g_vector - g_vector = jnp.zeros(3) + g_vector = jnp.zeros(3, dtype=dtype) g_vector = g_vector.at[random_index].set(g) new_r_cart = old_r_cart + g_vector @@ -4616,7 +4657,7 @@ def body_fun(_, carry): # compute R_ratio R_ratio = (R_AS_ratio * WF_ratio) ** 2.0 - acceptance_ratio = jnp.min(jnp.array([1.0, R_ratio * T_ratio])) + acceptance_ratio = jnp.min(jnp.array([1.0, R_ratio * T_ratio], dtype=dtype)) jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) b = jax.random.uniform(subkey, shape=(), minval=0.0, maxval=1.0) @@ -4773,8 +4814,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - self.__latest_r_up_carts = jnp.array(r_carts_up) - self.__latest_r_dn_carts = jnp.array(r_carts_dn) + dtype = get_dtype("mcmc") + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -4859,6 +4901,9 @@ def run(self, num_mcmc_steps: int = 0) -> None: logger.info("This is a debugging class! It supposed to be very slow.") logger.info("") + dtype = get_dtype("mcmc") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + # MAIN MCMC loop from here !!! logger.info("Start MCMC") num_mcmc_done = 0 @@ -4945,7 +4990,7 @@ def run(self, num_mcmc_steps: int = 0) -> None: random_index = jax.random.randint(subkey, shape=(), minval=0, maxval=3) # plug g into g_vector - g_vector = np.zeros(3) + g_vector = np.zeros(3, dtype=dtype_np) g_vector[random_index] = g new_r_cart = old_r_cart + g_vector @@ -5021,7 +5066,7 @@ def run(self, num_mcmc_steps: int = 0) -> None: R_ratio = (R_AS_ratio * WF_ratio) ** 2.0 logger.devel(f"R_ratio, T_ratio = {R_ratio}, {T_ratio}") - acceptance_ratio = np.min(jnp.array([1.0, R_ratio * T_ratio])) + acceptance_ratio = np.min(jnp.array([1.0, R_ratio * T_ratio], dtype=dtype)) logger.devel(f"acceptance_ratio = {acceptance_ratio}") jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) @@ -5043,8 +5088,8 @@ def run(self, num_mcmc_steps: int = 0) -> None: # store vmapped outcomes self.__accepted_moves = self.__accepted_moves + np.sum(accepted_moves_nw) self.__rejected_moves = self.__rejected_moves + np.sum(rejected_moves_nw) - self.__latest_r_up_carts = jnp.array(latest_r_up_carts) - self.__latest_r_dn_carts = jnp.array(latest_r_dn_carts) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts, dtype=dtype) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts, dtype=dtype) self.__jax_PRNG_key_list = jnp.array(jax_PRNG_key_list) # generate rotation matrices (for non-local ECPs) @@ -5065,11 +5110,12 @@ def run(self, num_mcmc_steps: int = 0) -> None: [cos_b * cos_g, cos_g * sin_a * sin_b - cos_a * sin_g, sin_a * sin_g + cos_a * cos_g * sin_b], [cos_b * sin_g, cos_a * cos_g + sin_a * sin_b * sin_g, cos_a * sin_b * sin_g - cos_g * sin_a], [-sin_b, cos_b * sin_a, cos_a * cos_b], - ] + ], + dtype=dtype, ) RTs.append(R.T) else: - RTs.append(jnp.eye(3)) + RTs.append(jnp.eye(3, dtype=dtype)) RTs = jnp.array(RTs) # evaluate observables @@ -5167,10 +5213,10 @@ def run(self, num_mcmc_steps: int = 0) -> None: n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn n_atoms = self.__hamiltonian_data.structure_data.natom - omega_up = jnp.zeros((self.__num_walkers, n_atoms, n_up)) - omega_dn = jnp.zeros((self.__num_walkers, n_atoms, n_dn)) - grad_omega_dr_up = jnp.zeros((self.__num_walkers, n_atoms, 3)) - grad_omega_dr_dn = jnp.zeros((self.__num_walkers, n_atoms, 3)) + omega_up = jnp.zeros((self.__num_walkers, n_atoms, n_up), dtype=dtype) + omega_dn = jnp.zeros((self.__num_walkers, n_atoms, n_dn), dtype=dtype) + grad_omega_dr_up = jnp.zeros((self.__num_walkers, n_atoms, 3), dtype=dtype) + grad_omega_dr_dn = jnp.zeros((self.__num_walkers, n_atoms, 3), dtype=dtype) self.__stored_omega_up.append(omega_up) self.__stored_omega_dn.append(omega_dn) diff --git a/jqmc/jqmc_tool.py b/jqmc/jqmc_tool.py index ba70d6e4..0f387deb 100644 --- a/jqmc/jqmc_tool.py +++ b/jqmc/jqmc_tool.py @@ -1,4 +1,10 @@ -"""jQMC tools.""" +"""jQMC tools. + +Precision Zones: + - ``io``: all functions in this module. + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano @@ -58,6 +64,7 @@ load_hamiltonian_from_checkpoint, load_observables_from_checkpoint, ) +from ._precision import get_dtype from ._setting import ( GFMC_MIN_BIN_BLOCKS, GFMC_MIN_COLLECT_STEPS, @@ -406,8 +413,9 @@ def _get_j3_shell_counts(z: int, j3_choice: str) -> dict[int, int] | None: # 10) Reconstruct all common dataclass fields for the new AO object new_orbital_indices = [new_idx_map[aos_data.orbital_indices[p]] for p in new_prims] - new_exponents = jnp.array([float(aos_data.exponents[p]) for p in new_prims], dtype=jnp.float64) - new_coefficients = jnp.array([float(aos_data.coefficients[p]) for p in new_prims], dtype=jnp.float64) + dtype_io = get_dtype("io") + new_exponents = jnp.array([float(aos_data.exponents[p]) for p in new_prims], dtype=dtype_io) + new_coefficients = jnp.array([float(aos_data.coefficients[p]) for p in new_prims], dtype=dtype_io) new_nucleus_index = [aos_data.nucleus_index[i] for i in selected_ao_indices] new_angular_momentums = [aos_data.angular_momentums[i] for i in selected_ao_indices] diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index dfdf8dd5..e8de77cb 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -1,4 +1,11 @@ -"""Molecular Orbital module.""" +"""Molecular Orbital module. + +Precision Zones: + - ``orb_eval``: forward MO evaluation (compute_MOs). + - ``kinetic``: MO gradient and Laplacian (compute_MOs_grad, compute_MOs_laplacian). + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -46,6 +53,7 @@ from jax import typing as jnpt # myqmc module +from ._precision import get_dtype from .atomic_orbital import ( AOs_cart_data, AOs_sphe_data, @@ -180,8 +188,9 @@ def to_cartesian(self) -> "MOs_data": return self if not isinstance(self.aos_data, AOs_sphe_data): raise ValueError("Cartesian conversion is only available from spherical AOs.") + dtype = get_dtype("orb_eval") aos_cart, transform_matrix = _aos_sphe_to_cart(self.aos_data) - cart_coeffs = np.asarray(self.mo_coefficients, dtype=np.float64) @ transform_matrix + cart_coeffs = np.asarray(self.mo_coefficients, dtype=dtype) @ transform_matrix cart_coeffs = cart_coeffs.astype(np.asarray(self.mo_coefficients).dtype, copy=False) return MOs_data(num_mo=self.num_mo, aos_data=aos_cart, mo_coefficients=cart_coeffs) @@ -203,8 +212,9 @@ def to_spherical(self) -> "MOs_data": return self if not isinstance(self.aos_data, AOs_cart_data): raise ValueError("Spherical conversion is only available from Cartesian AOs.") + dtype = get_dtype("orb_eval") aos_sphe, transform_pinv = _aos_cart_to_sphe(self.aos_data) - sph_coeffs = np.asarray(self.mo_coefficients, dtype=np.float64) @ transform_pinv + sph_coeffs = np.asarray(self.mo_coefficients, dtype=dtype) @ transform_pinv sph_coeffs = sph_coeffs.astype(np.asarray(self.mo_coefficients).dtype, copy=False) return MOs_data(num_mo=self.num_mo, aos_data=aos_sphe, mo_coefficients=sph_coeffs) diff --git a/jqmc/structure.py b/jqmc/structure.py index c5aabf40..1c057546 100755 --- a/jqmc/structure.py +++ b/jqmc/structure.py @@ -1,4 +1,10 @@ -"""Structure module.""" +"""Structure module. + +Precision Zones: + - ``io``: all functions in this module. + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -47,6 +53,8 @@ from jax import typing as jnpt from numpy import linalg as LA +from ._precision import get_dtype + # set logger logger = getLogger("jqmc").getChild(__name__) @@ -179,13 +187,14 @@ def _logger_info(self) -> None: logger.info(line) @property - def cell(self) -> npt.NDArray[np.float64]: + def cell(self) -> npt.NDArray: """Lattice vectors as a ``(3, 3)`` matrix in Bohr (``[a, b, c]``).""" - cell = np.array([self.vec_a, self.vec_b, self.vec_c]) + dtype = get_dtype("io") + cell = np.array([self.vec_a, self.vec_b, self.vec_c], dtype=dtype) return cell @property - def recip_cell(self) -> npt.NDArray[np.float64]: + def recip_cell(self) -> npt.NDArray: r"""Reciprocal lattice vectors ``(3, 3)`` in Bohr^{-1}. Uses the standard definition @@ -197,12 +206,16 @@ def recip_cell(self) -> npt.NDArray[np.float64]: and asserts the orthonormality condition :math:`T_i \cdot G_j = 2\pi\,\delta_{ij}`. """ - recip_a = 2 * np.pi * (np.cross(self.vec_b, self.vec_c)) / (np.dot(self.vec_a, np.cross(self.vec_b, self.vec_c))) - recip_b = 2 * np.pi * (np.cross(self.vec_c, self.vec_a)) / (np.dot(self.vec_b, np.cross(self.vec_c, self.vec_a))) - recip_c = 2 * np.pi * (np.cross(self.vec_a, self.vec_b)) / (np.dot(self.vec_c, np.cross(self.vec_a, self.vec_b))) + dtype = get_dtype("io") + va = np.asarray(self.vec_a, dtype=dtype) + vb = np.asarray(self.vec_b, dtype=dtype) + vc = np.asarray(self.vec_c, dtype=dtype) + recip_a = 2 * np.pi * (np.cross(vb, vc)) / (np.dot(va, np.cross(vb, vc))) + recip_b = 2 * np.pi * (np.cross(vc, va)) / (np.dot(vb, np.cross(vc, va))) + recip_c = 2 * np.pi * (np.cross(va, vb)) / (np.dot(vc, np.cross(va, vb))) # check if the implementations are correct - lattice_vec_list = [self.vec_a, self.vec_b, self.vec_c] + lattice_vec_list = [va, vb, vc] recip_vec_list = [recip_a, recip_b, recip_c] for (lattice_vec_i, lattice_vec), (recip_vec_j, recip_vec) in itertools.product( enumerate(lattice_vec_list), enumerate(recip_vec_list) @@ -212,7 +225,7 @@ def recip_cell(self) -> npt.NDArray[np.float64]: else: np.testing.assert_almost_equal(np.dot(lattice_vec, recip_vec), 0.0, decimal=15) - recip_cell = np.array([recip_a, recip_b, recip_c]) + recip_cell = np.array([recip_a, recip_b, recip_c], dtype=dtype) return recip_cell @property @@ -306,20 +319,26 @@ def norm_vec_c(self) -> float: return LA.norm(self.vec_c) @property - def _positions_cart_np(self) -> npt.NDArray[np.float64]: + def _positions_cart_np(self) -> npt.NDArray: """Atomic positions as ``numpy.ndarray`` with shape ``(N, 3)`` in Bohr.""" - return np.array(self.positions) + dtype = get_dtype("io") + return np.array(self.positions, dtype=dtype) @property def _positions_cart_jnp(self) -> jax.Array: """Atomic positions as ``jax.Array`` with shape ``(N, 3)`` in Bohr.""" - return jnp.array(self.positions) + dtype = get_dtype("io") + return jnp.array(self.positions, dtype=dtype) @property - def _positions_frac_np(self) -> npt.NDArray[np.float64]: + def _positions_frac_np(self) -> npt.NDArray: """Fractional (crystal) coordinates as ``numpy.ndarray`` with shape ``(N, 3)``.""" - h = np.array([self.vec_a, self.vec_b, self.vec_c]) - positions_frac = np.array([np.dot(np.array(pos), np.linalg.inv(h)) for pos in self._positions_cart_np]) + dtype = get_dtype("io") + h = np.array([self.vec_a, self.vec_b, self.vec_c], dtype=dtype) + positions_frac = np.array( + [np.dot(np.asarray(pos, dtype=dtype), np.linalg.inv(h)) for pos in self._positions_cart_np], + dtype=dtype, + ) return positions_frac @property @@ -375,8 +394,9 @@ def _find_nearest_index_jnp(structure: Structure_data, r_cart: list[float]) -> i def _find_nearest_nucleus_indices_np(structure_data: Structure_data, r_cart, N): """See find_nearest_index.""" + dtype = get_dtype("io") positions = structure_data._positions_cart_np - r_cart = np.array(r_cart) + r_cart = np.array(r_cart, dtype=dtype) diffs = positions - r_cart if structure_data.pbc_flag: cell = structure_data.cell @@ -393,11 +413,12 @@ def _find_nearest_nucleus_indices_np(structure_data: Structure_data, r_cart, N): @partial(jit, static_argnums=2) def _find_nearest_nucleus_indices_jnp(structure_data: Structure_data, r_cart, N): """See find_nearest_index.""" + dtype = get_dtype("io") positions = structure_data._positions_cart_jnp - r_cart = jnp.array(r_cart) + r_cart = jnp.array(r_cart, dtype=dtype) diffs = positions - r_cart if structure_data.pbc_flag: - cell = jnp.array(structure_data.cell) + cell = jnp.array(structure_data.cell, dtype=dtype) inv_cell = jnp.linalg.inv(cell) diffs_frac = diffs @ inv_cell diffs_frac = diffs_frac - jnp.round(diffs_frac) @@ -421,7 +442,8 @@ def _get_min_dist_rel_R_cart_np(structure_data: Structure_data, r_cart: list[flo with respect to the given r_cart in cartesian. The unit is Bohr """ - r_cart = np.array(r_cart) + dtype = get_dtype("io") + r_cart = np.array(r_cart, dtype=dtype) R_cart = structure_data._positions_cart_np[i_atom] diff = R_cart - r_cart if structure_data.pbc_flag: @@ -436,11 +458,12 @@ def _get_min_dist_rel_R_cart_np(structure_data: Structure_data, r_cart: list[flo @jit def _get_min_dist_rel_R_cart_jnp(structure_data: Structure_data, r_cart: list[float, float, float], i_atom: int) -> float: """See get_min_dist_rel_R_cart_np.""" - r_cart = jnp.array(r_cart) - R_cart = jnp.array(structure_data._positions_cart_jnp[i_atom]) + dtype = get_dtype("io") + r_cart = jnp.array(r_cart, dtype=dtype) + R_cart = jnp.array(structure_data._positions_cart_jnp[i_atom], dtype=dtype) diff = R_cart - r_cart if structure_data.pbc_flag: - cell = jnp.array(structure_data.cell) + cell = jnp.array(structure_data.cell, dtype=dtype) inv_cell = jnp.linalg.inv(cell) diff_frac = diff @ inv_cell diff_frac = diff_frac - jnp.round(diff_frac) diff --git a/jqmc/swct.py b/jqmc/swct.py index 207b361a..82395e09 100644 --- a/jqmc/swct.py +++ b/jqmc/swct.py @@ -1,4 +1,10 @@ -"""SWCT module.""" +"""SWCT module. + +Precision Zones: + - ``kinetic``: all functions in this module. + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -45,6 +51,7 @@ from jax import typing as jnpt # jQMC modules +from ._precision import get_dtype from .structure import Structure_data # set logger @@ -68,7 +75,9 @@ def evaluate_swct_omega( Returns: jax.Array: Normalized weights with shape ``(N_a, N_e)``, summing to 1 over atoms for each electron. """ - R_carts = structure_data._positions_cart_jnp + dtype = get_dtype("kinetic") + r_carts = jnp.asarray(r_carts, dtype=dtype) + R_carts = jnp.asarray(structure_data._positions_cart_jnp, dtype=dtype) def compute_omega(R_cart, r_cart): kappa = 1.0 / jnp.linalg.norm(r_cart - R_cart) ** 4 @@ -93,8 +102,11 @@ def _evaluate_swct_omega_debug( r_carts: npt.NDArray[np.float64], ) -> npt.NDArray[np.float64]: """NumPy fallback for ``evaluate_swct_omega`` used in debug paths.""" + dtype = get_dtype("kinetic") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_carts = np.asarray(r_carts, dtype=dtype_np) R_carts = structure_data._positions_cart_np - omega = np.zeros((len(R_carts), len(r_carts))) + omega = np.zeros((len(R_carts), len(r_carts)), dtype=dtype_np) for alpha in range(len(R_carts)): for i in range(len(r_carts)): @@ -119,6 +131,8 @@ def evaluate_swct_domega( Returns: jax.Array: Sum of gradients per atom with shape ``(N_a, 3)``. """ + dtype = get_dtype("kinetic") + r_carts = jnp.asarray(r_carts, dtype=dtype) domega = jnp.sum(jacrev(evaluate_swct_omega, argnums=1)(structure_data, r_carts), axis=(1, 2)) return domega @@ -129,8 +143,11 @@ def _evaluate_swct_domega_debug( r_carts: npt.NDArray[np.float64], ) -> npt.NDArray[np.float64]: """NumPy fallback for ``evaluate_swct_domega`` used in debug paths.""" + dtype = get_dtype("kinetic") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + r_carts = np.asarray(r_carts, dtype=dtype_np) R_carts = structure_data._positions_cart_np - domega = np.zeros((len(R_carts), 3)) + domega = np.zeros((len(R_carts), 3), dtype=dtype_np) def compute_omega(R_cart, r_cart, R_carts): kappa = 1.0 / np.linalg.norm(r_cart - R_cart) ** 4 diff --git a/jqmc/trexio_wrapper.py b/jqmc/trexio_wrapper.py index 6ba27de8..f9cf0749 100644 --- a/jqmc/trexio_wrapper.py +++ b/jqmc/trexio_wrapper.py @@ -1,4 +1,10 @@ -"""TREXIO wrapper modules.""" +"""TREXIO wrapper modules. + +Precision Zones: + - ``io``: all functions in this module. + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -45,6 +51,7 @@ # import trexio import trexio +from ._precision import get_dtype from .atomic_orbital import AOs_cart_data, AOs_sphe_data from .coulomb_potential import Coulomb_potential_data from .determinant import Geminal_data @@ -91,8 +98,7 @@ def read_trexio_file( >>> structure_data.atomic_labels[:3] ['O', 'H', 'H'] """ - # prefix and file names - # logger.info(f"TREXIO file = {trexio_file}") + dtype = get_dtype("io") # read a trexio file file_r = trexio.File( @@ -210,7 +216,7 @@ def read_trexio_file( atomic_numbers=tuple(_convert_from_atomic_labels_to_atomic_numbers(labels_r)), element_symbols=tuple(labels_r), atomic_labels=tuple(labels_r), - positions=np.array(coords_r), + positions=np.array(coords_r, dtype=dtype), ) else: structure_data = Structure_data( @@ -221,7 +227,7 @@ def read_trexio_file( atomic_numbers=list(_convert_from_atomic_labels_to_atomic_numbers(labels_r)), element_symbols=list(labels_r), atomic_labels=list(labels_r), - positions=np.array(coords_r), + positions=np.array(coords_r, dtype=dtype), ) # ao spherical part check @@ -333,8 +339,8 @@ def read_trexio_file( polynominal_order_y=tuple(polynominal_order_y), polynominal_order_z=tuple(polynominal_order_z), orbital_indices=tuple(orbital_indices), - exponents=jnp.array(exponents, dtype=jnp.float64), - coefficients=jnp.array(coefficients, dtype=jnp.float64), + exponents=jnp.array(exponents, dtype=dtype), + coefficients=jnp.array(coefficients, dtype=dtype), ) else: aos_data = AOs_cart_data( @@ -347,8 +353,8 @@ def read_trexio_file( polynominal_order_y=list(polynominal_order_y), polynominal_order_z=list(polynominal_order_z), orbital_indices=list(orbital_indices), - exponents=jnp.array(exponents, dtype=jnp.float64), - coefficients=jnp.array(coefficients, dtype=jnp.float64), + exponents=jnp.array(exponents, dtype=dtype), + coefficients=jnp.array(coefficients, dtype=dtype), ) else: logger.debug("Spherical basis functions.") @@ -429,8 +435,8 @@ def read_trexio_file( angular_momentums=tuple(angular_momentums), magnetic_quantum_numbers=tuple(magnetic_quantum_numbers), orbital_indices=tuple(orbital_indices), - exponents=jnp.array(exponents, dtype=jnp.float64), - coefficients=jnp.array(coefficients, dtype=jnp.float64), + exponents=jnp.array(exponents, dtype=dtype), + coefficients=jnp.array(coefficients, dtype=dtype), ) else: aos_data = AOs_sphe_data( @@ -441,8 +447,8 @@ def read_trexio_file( angular_momentums=list(angular_momentums), magnetic_quantum_numbers=list(magnetic_quantum_numbers), orbital_indices=list(orbital_indices), - exponents=jnp.array(exponents, dtype=jnp.float64), - coefficients=jnp.array(coefficients, dtype=jnp.float64), + exponents=jnp.array(exponents, dtype=dtype), + coefficients=jnp.array(coefficients, dtype=dtype), ) # MOs_data instance @@ -466,11 +472,11 @@ def read_trexio_file( num_ele_diff = num_ele_up - num_ele_dn mo_lambda_paired = np.pad( - np.eye(num_ele_dn, dtype=np.float64), ((0, mo_considered_num - num_ele_dn), (0, mo_considered_num - num_ele_dn)) + np.eye(num_ele_dn, dtype=dtype), ((0, mo_considered_num - num_ele_dn), (0, mo_considered_num - num_ele_dn)) ) mo_lambda_unpaired = np.pad( - np.eye(num_ele_diff, dtype=np.float64), ((num_ele_dn, mo_considered_num - num_ele_dn - num_ele_diff), (0, 0)) + np.eye(num_ele_diff, dtype=dtype), ((num_ele_dn, mo_considered_num - num_ele_dn - num_ele_diff), (0, 0)) ) mo_lambda_matrix = np.hstack([mo_lambda_paired, mo_lambda_unpaired]) @@ -502,11 +508,11 @@ def read_trexio_file( num_ele_diff = num_ele_up - num_ele_dn mo_lambda_paired = np.pad( - np.eye(num_ele_dn, dtype=np.float64), ((0, mo_considered_num - num_ele_dn), (0, mo_considered_num - num_ele_dn)) + np.eye(num_ele_dn, dtype=dtype), ((0, mo_considered_num - num_ele_dn), (0, mo_considered_num - num_ele_dn)) ) mo_lambda_unpaired = np.pad( - np.eye(num_ele_diff, dtype=np.float64), ((num_ele_dn, mo_considered_num - num_ele_dn - num_ele_diff), (0, 0)) + np.eye(num_ele_diff, dtype=dtype), ((num_ele_dn, mo_considered_num - num_ele_dn - num_ele_diff), (0, 0)) ) mo_lambda_matrix = np.hstack([mo_lambda_paired, mo_lambda_unpaired]) else: diff --git a/jqmc/wavefunction.py b/jqmc/wavefunction.py index acbc12bd..5d4ed6f3 100644 --- a/jqmc/wavefunction.py +++ b/jqmc/wavefunction.py @@ -1,4 +1,12 @@ -"""Wavefunction module.""" +"""Wavefunction module. + +Precision Zones: + - ``kinetic``: kinetic-energy evaluation (compute_kinetic_energy*). + - Zone-boundary casts when combining results from ``jastrow`` and + ``determinant`` zones. + +See :mod:`jqmc._precision` for details. +""" # Copyright (C) 2024- Kosuke Nakano # All rights reserved. @@ -64,6 +72,7 @@ compute_Jastrow_part, ) from .molecular_orbital import MOs_data +from ._precision import get_dtype # set logger logger = getLogger("jqmc").getChild(__name__) @@ -662,8 +671,8 @@ def evaluate_ln_wavefunction( This follows the original behavior: compute the Jastrow part, multiply the determinant part, and then take ``log(abs(det))`` while keeping the full - Jastrow contribution. The inputs are converted to float64 ``jax.Array`` for - downstream consistency. + Jastrow contribution. The inputs are converted to the determinant zone dtype + ``jax.Array`` for downstream consistency. Args: wavefunction_data: Wavefunction parameters (Jastrow + Geminal). @@ -673,8 +682,9 @@ def evaluate_ln_wavefunction( Returns: Scalar log-value of the wavefunction magnitude. """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype_det = get_dtype("determinant") + r_up = jnp.asarray(r_up_carts, dtype=dtype_det) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype_det) Jastrow_part = compute_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, @@ -688,7 +698,7 @@ def evaluate_ln_wavefunction( r_dn_carts=r_dn, ) - return Jastrow_part + jnp.log(jnp.abs(Determinant_part)) + return jnp.asarray(Jastrow_part, dtype=dtype_det) + jnp.log(jnp.abs(Determinant_part)) def evaluate_ln_wavefunction_fast( @@ -726,8 +736,9 @@ def evaluate_ln_wavefunction_fast( Passing an inverse from a different configuration silently produces incorrect parameter gradients (``O_matrix`` / SR). """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype_det = get_dtype("determinant") + r_up = jnp.asarray(r_up_carts, dtype=dtype_det) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype_det) Jastrow_part = compute_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, @@ -742,7 +753,7 @@ def evaluate_ln_wavefunction_fast( geminal_inv, ) - return Jastrow_part + ln_det + return jnp.asarray(Jastrow_part, dtype=dtype_det) + ln_det @jit @@ -754,8 +765,8 @@ def evaluate_wavefunction( """Evaluate the wavefunction ``Psi`` at given electron coordinates. The method is for evaluate wavefunction (Psi) at ``(r_up_carts, r_dn_carts)`` and - returns ``exp(Jastrow) * Determinant``. Inputs are coerced to float64 - ``jax.Array`` to match other compute utilities. + returns ``exp(Jastrow) * Determinant``. Inputs are coerced to the determinant + zone dtype ``jax.Array`` to match other compute utilities. Args: wavefunction_data: Wavefunction parameters (Jastrow + Geminal). @@ -765,8 +776,9 @@ def evaluate_wavefunction( Returns: Complex or real wavefunction value. """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype_det = get_dtype("determinant") + r_up = jnp.asarray(r_up_carts, dtype=dtype_det) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype_det) Jastrow_part = compute_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, @@ -780,7 +792,7 @@ def evaluate_wavefunction( r_dn_carts=r_dn, ) - return jnp.exp(Jastrow_part) * Determinant_part + return jnp.exp(jnp.asarray(Jastrow_part, dtype=dtype_det)) * Determinant_part def evaluate_jastrow( @@ -802,8 +814,9 @@ def evaluate_jastrow( Returns: Real Jastrow factor ``exp(J)``. """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype = get_dtype("jastrow") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) Jastrow_part = compute_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, @@ -829,8 +842,9 @@ def evaluate_determinant( Returns: Determinant value evaluated at the supplied coordinates. """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype = get_dtype("determinant") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) Determinant_part = compute_det_geminal_all_elements( geminal_data=wavefunction_data.geminal_data, @@ -851,8 +865,8 @@ def compute_kinetic_energy( The method is for computing kinetic energy of the given WF at ``(r_up_carts, r_dn_carts)`` and fully exploits the JAX library for the - kinetic energy calculation. Inputs are converted to float64 ``jax.Array`` - for consistency with other compute utilities. + kinetic energy calculation. Inputs are converted to the kinetic zone dtype + ``jax.Array`` for consistency with other compute utilities. Args: wavefunction_data: Wavefunction parameters (Jastrow + Geminal). @@ -862,8 +876,9 @@ def compute_kinetic_energy( Returns: Kinetic energy evaluated for the supplied configuration. """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype = get_dtype("kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) # grad_J_up, grad_J_dn, sum_laplacian_J = 0.0, 0.0, 0.0 # """ @@ -917,8 +932,9 @@ def _compute_kinetic_energy_auto( Returns: The kinetic energy with the given wavefunction (float | complex) """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype = get_dtype("kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) kinetic_energy_all_elements_up, kinetic_energy_all_elements_dn = _compute_kinetic_energy_all_elements_auto( wavefunction_data=wavefunction_data, r_up_carts=r_up, r_dn_carts=r_dn @@ -994,8 +1010,9 @@ def _compute_kinetic_energy_all_elements_auto( r_dn_carts: jax.Array, ) -> jax.Array: """See compute_kinetic_energy_api.""" - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype = get_dtype("kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) # compute gradients grad_J_up = grad(compute_Jastrow_part, argnums=1)(wavefunction_data.jastrow_data, r_up, r_dn) @@ -1047,8 +1064,9 @@ def compute_kinetic_energy_all_elements( Tuple of two ``jax.Array`` objects containing per-electron kinetic energies for spin-up and spin-down electrons, respectively. """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype = get_dtype("kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) # --- Jastrow contributions (per-electron Laplacians) --- grad_J_up, grad_J_dn, lap_J_up, lap_J_dn = compute_grads_and_laplacian_Jastrow_part( @@ -1108,8 +1126,9 @@ def compute_kinetic_energy_all_elements_fast_update( if geminal_inverse is None: raise ValueError("geminal_inverse must be provided for fast update") - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype = get_dtype("kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) grad_J_up, grad_J_dn, lap_J_up, lap_J_dn = compute_grads_and_laplacian_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, @@ -1246,7 +1265,7 @@ def compute_discretized_kinetic_energy( Function for computing discretized kinetic grid points and their energies with a given lattice space (alat). This keeps the original semantics used by the LRDMC path: ratios are computed as ``exp(J_xp - J_x) * det_xp / det_x``. Inputs are - coerced to float64 ``jax.Array`` before evaluation. + coerced to the kinetic zone dtype ``jax.Array`` before evaluation. Args: alat: Hamiltonian discretization (bohr), which will be replaced with ``LRDMC_data``. @@ -1260,9 +1279,10 @@ def compute_discretized_kinetic_energy( combined coordinate arrays have shapes ``(n_grid, n_up, 3)`` and ``(n_grid, n_dn, 3)`` and ``elements_kinetic_part`` contains the kinetic prefactor-scaled ratios. """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) - rt = jnp.asarray(RT, dtype=jnp.float64) + dtype = get_dtype("kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + rt = jnp.asarray(RT, dtype=dtype) # Define the shifts to apply (+/- alat in each coordinate direction) shifts = alat * jnp.array( [ @@ -1361,8 +1381,8 @@ def compute_discretized_kinetic_energy_fast_update( Function for computing discretized kinetic grid points and their energies with a given lattice space (alat). Uses precomputed ``A_old_inv`` to evaluate - determinant ratios efficiently. Inputs are converted to float64 ``jax.Array`` - before use. + determinant ratios efficiently. Inputs are converted to the kinetic zone dtype + ``jax.Array`` before use. Args: alat: Hamiltonian discretization (bohr), which will be replaced with ``LRDMC_data``. @@ -1377,9 +1397,10 @@ def compute_discretized_kinetic_energy_fast_update( coordinate arrays of shapes ``(n_grid, n_up, 3)`` and ``(n_grid, n_dn, 3)``, and kinetic prefactor-scaled ratios ``elements_kinetic_part``. """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) - rt = jnp.asarray(RT, dtype=jnp.float64) + dtype = get_dtype("kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + rt = jnp.asarray(RT, dtype=dtype) # Define the shifts to apply (+/- alat in each coordinate direction) shifts = alat * jnp.array( [ @@ -1516,8 +1537,9 @@ def compute_nodal_distance( Returns: Scalar nodal distance value. """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype = get_dtype("determinant") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) grad_J_up, grad_J_dn, _, _ = compute_grads_and_laplacian_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, @@ -1564,8 +1586,9 @@ def _compute_nodal_distance_debug( Returns: Scalar nodal distance value. """ - r_up = jnp.asarray(r_up_carts, dtype=jnp.float64) - r_dn = jnp.asarray(r_dn_carts, dtype=jnp.float64) + dtype = get_dtype("determinant") + r_up = jnp.asarray(r_up_carts, dtype=dtype) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype) Psi = evaluate_wavefunction(wavefunction_data, r_up, r_dn) diff --git a/jqmc_workflow/lrdmc_workflow.py b/jqmc_workflow/lrdmc_workflow.py index a210ca0d..9b578ac9 100644 --- a/jqmc_workflow/lrdmc_workflow.py +++ b/jqmc_workflow/lrdmc_workflow.py @@ -324,6 +324,9 @@ def __init__( pilot_queue_label: Optional[str] = None, max_continuation: int = 1, cleanup_patterns: Optional[list] = None, + # -- [precision] section -- + precision_mode: Optional[str] = None, + precision_overrides: Optional[dict] = None, ): super().__init__(cleanup_patterns=cleanup_patterns) self.server_machine_name = server_machine_name @@ -370,6 +373,9 @@ def __init__( self.num_gfmc_projections = num_gfmc_projections self.pilot_queue_label = pilot_queue_label or queue_label self.max_continuation = max_continuation + # [precision] section + self.precision_mode = precision_mode + self.precision_overrides = precision_overrides @property def job_type(self) -> str: @@ -458,6 +464,14 @@ def _generate_input( "control": control_ov, jt: section_ov, } + # Add [precision] section if configured + if self.precision_mode is not None or self.precision_overrides: + precision_ov = {} + if self.precision_mode is not None: + precision_ov["mode"] = self.precision_mode + if self.precision_overrides: + precision_ov.update(self.precision_overrides) + overrides["precision"] = precision_ov generate_input_toml( job_type=jt, overrides=overrides, diff --git a/jqmc_workflow/mcmc_workflow.py b/jqmc_workflow/mcmc_workflow.py index f11c0fe7..21b4d849 100644 --- a/jqmc_workflow/mcmc_workflow.py +++ b/jqmc_workflow/mcmc_workflow.py @@ -247,6 +247,9 @@ def __init__( pilot_queue_label: Optional[str] = None, max_continuation: int = 1, cleanup_patterns: Optional[list] = None, + # -- [precision] section -- + precision_mode: Optional[str] = None, + precision_overrides: Optional[dict] = None, ): super().__init__(cleanup_patterns=cleanup_patterns) self.server_machine_name = server_machine_name @@ -274,6 +277,9 @@ def __init__( self.pilot_steps = pilot_steps self.pilot_queue_label = pilot_queue_label or queue_label self.max_continuation = max_continuation + # [precision] section + self.precision_mode = precision_mode + self.precision_overrides = precision_overrides # ── Input generation ────────────────────────────────────────── @@ -316,6 +322,14 @@ def _generate_input( "control": control_ov, "mcmc": mcmc_ov, } + # Add [precision] section if configured + if self.precision_mode is not None or self.precision_overrides: + precision_ov = {} + if self.precision_mode is not None: + precision_ov["mode"] = self.precision_mode + if self.precision_overrides: + precision_ov.update(self.precision_overrides) + overrides["precision"] = precision_ov generate_input_toml( job_type="mcmc", overrides=overrides, diff --git a/jqmc_workflow/vmc_workflow.py b/jqmc_workflow/vmc_workflow.py index c21dc541..2c00742e 100644 --- a/jqmc_workflow/vmc_workflow.py +++ b/jqmc_workflow/vmc_workflow.py @@ -311,6 +311,9 @@ def __init__( energy_slope_sigma_threshold: Optional[float] = None, energy_slope_window_size: int = 5, cleanup_patterns: Optional[list] = None, + # -- [precision] section -- + precision_mode: Optional[str] = None, + precision_overrides: Optional[dict] = None, ): super().__init__(cleanup_patterns=cleanup_patterns) self.server_machine_name = server_machine_name @@ -353,6 +356,9 @@ def __init__( self.snr_avg_window = snr_avg_window self.energy_slope_sigma_threshold = energy_slope_sigma_threshold self.energy_slope_window_size = energy_slope_window_size + # [precision] section + self.precision_mode = precision_mode + self.precision_overrides = precision_overrides # ── Input generation ────────────────────────────────────────── @@ -420,6 +426,14 @@ def _generate_input( "control": control_ov, "vmc": vmc_ov, } + # Add [precision] section if configured + if self.precision_mode is not None or self.precision_overrides: + precision_ov = {} + if self.precision_mode is not None: + precision_ov["mode"] = self.precision_mode + if self.precision_overrides: + precision_ov.update(self.precision_overrides) + overrides["precision"] = precision_ov generate_input_toml( job_type="vmc", overrides=overrides, diff --git a/tests/conftest.py b/tests/conftest.py index eb441fce..2d67bc79 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,12 @@ def pytest_addoption(parser): """Add options for pytests.""" parser.addoption("--disable-jit", action="store_true", default=False, help="Disable jax.jit for pytests") parser.addoption("--skip-heavy", action="store_true", default=False, help="Skip heavy calculations for pytests") + parser.addoption( + "--precision-mode", + default="full", + choices=["full", "mixed"], + help="Precision mode for tests (default: full)", + ) @pytest.fixture(autouse=True) @@ -23,6 +29,15 @@ def enable_jit(request): jax.config.update("jax_disable_jit", False) +@pytest.fixture(autouse=True) +def configure_precision(request): + """Configure precision mode before each test.""" + from jqmc._precision import configure + + mode = request.config.getoption("--precision-mode") + configure({"mode": mode}) + + def pytest_itemcollected(item): """Show reason for obsolete tests.""" obsolete_marker = item.get_closest_marker("obsolete") @@ -37,13 +52,36 @@ def pytest_configure(config): config.addinivalue_line("markers", "activate_if_disable_jit: activate test if --disable-jit is set") config.addinivalue_line("markers", "activate_if_skip_heavy: skip test if --skip-heavy is set") config.addinivalue_line("markers", "obsolete: tests that are obsolete and should be removed in the future") + config.addinivalue_line( + "markers", + "numerical_diff: test compares analytic or autodiff results " + "against finite-difference derivatives or numerical quadrature. " + "Skipped automatically when --precision-mode=mixed because " + "float32 round-off dominates the FD / quadrature error.", + ) + config.addinivalue_line( + "markers", + "external_reference: test compares against an external reference " + "(e.g. TurboRVB). Validated only in --precision-mode=full; " + "skipped in mixed mode.", + ) def pytest_collection_modifyitems(config, items): - """Skip tests marked with activate_if_skip_heavy when --skip-heavy is set.""" - if not config.getoption("--skip-heavy"): - return - skip_marker = pytest.mark.skip(reason="skipped by --skip-heavy") - for item in items: - if item.get_closest_marker("activate_if_skip_heavy"): - item.add_marker(skip_marker) + """Skip tests based on CLI options (--skip-heavy, --precision-mode).""" + if config.getoption("--skip-heavy"): + skip_marker = pytest.mark.skip(reason="skipped by --skip-heavy") + for item in items: + if item.get_closest_marker("activate_if_skip_heavy"): + item.add_marker(skip_marker) + + if config.getoption("--precision-mode") == "mixed": + skip_fd = pytest.mark.skip( + reason="FD / numerical-quadrature comparison is invalid under mixed precision (float32 round-off dominates)." + ) + skip_extref = pytest.mark.skip(reason="External-reference comparison validated only in mode=full.") + for item in items: + if "numerical_diff" in item.keywords: + item.add_marker(skip_fd) + if "external_reference" in item.keywords: + item.add_marker(skip_extref) diff --git a/tests/test_AOs.py b/tests/test_AOs.py index 80126184..c19d9e67 100755 --- a/tests/test_AOs.py +++ b/tests/test_AOs.py @@ -68,18 +68,7 @@ compute_AOs_laplacian, compute_overlap_matrix, ) -from jqmc._setting import ( # noqa: E402 - atol_auto_vs_analytic_deriv, - rtol_auto_vs_analytic_deriv, - atol_auto_vs_numerical_deriv, - rtol_auto_vs_numerical_deriv, - atol_consistency, - rtol_consistency, - atol_debug_vs_production, - rtol_debug_vs_production, - atol_numerical_vs_analytic_deriv, - rtol_numerical_vs_analytic_deriv, -) +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.structure import Structure_data # noqa: E402 # JAX float64 @@ -220,6 +209,7 @@ def Y_l_m_ref(l=0, m=0, r_cart_rel=None): r_y_rand = (r_cart_max - r_cart_min) * np.random.rand(num_samples) + r_cart_min r_z_rand = (r_cart_max - r_cart_min) * np.random.rand(num_samples) + r_cart_min + atol, rtol = get_tolerance("orb_eval", "strict") for r_cart in zip(r_x_rand, r_y_rand, r_z_rand, strict=True): r_norm = LA.norm(np.array(R_cart) - np.array(r_cart)) r_cart_rel = np.array(r_cart) - np.array(R_cart) @@ -232,7 +222,7 @@ def Y_l_m_ref(l=0, m=0, r_cart_rel=None): ref_S_lm = np.sqrt((4 * np.pi) / (2 * l + 1)) * r_norm**l * Y_l_m_ref(l=l, m=m, r_cart_rel=r_cart_rel) assert not np.any(np.isnan(np.asarray(test_S_lm))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(ref_S_lm))), "NaN detected in second argument" - assert_allclose(test_S_lm, ref_S_lm, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + assert_allclose(test_S_lm, ref_S_lm, atol=atol, rtol=rtol) jax.clear_caches() @@ -273,9 +263,10 @@ def test_solid_harmonics_debug_vs_production(): # print(f"batch_S_l_m.shape = {batch_S_l_m.shape}.") + atol, rtol = get_tolerance("orb_eval", "strict") assert not np.any(np.isnan(np.asarray(S_l_m_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(S_l_m_jax))), "NaN detected in second argument" - np.testing.assert_allclose(S_l_m_debug, S_l_m_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(S_l_m_debug, S_l_m_jax, atol=atol, rtol=rtol) jax.clear_caches() @@ -326,12 +317,14 @@ def test_AOs_sphe_debug_vs_production(): ) aos_data.sanity_check() + atol, rtol = get_tolerance("orb_eval", "strict") + aos_jax = _compute_AOs_sphe(aos_data=aos_data, r_carts=r_carts) aos_debug = _compute_AOs_sphe_debug(aos_data=aos_data, r_carts=r_carts) assert not np.any(np.isnan(np.asarray(aos_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(aos_debug))), "NaN detected in second argument" - np.testing.assert_allclose(aos_jax, aos_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(aos_jax, aos_debug, atol=atol, rtol=rtol) num_el = 150 num_ao = len(ml_list) @@ -382,7 +375,7 @@ def test_AOs_sphe_debug_vs_production(): assert not np.any(np.isnan(np.asarray(aos_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(aos_debug))), "NaN detected in second argument" - np.testing.assert_allclose(aos_jax, aos_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(aos_jax, aos_debug, atol=atol, rtol=rtol) jax.clear_caches() @@ -451,12 +444,14 @@ def test_AOs_cart_debug_vs_production(): ) aos_data.sanity_check() + atol, rtol = get_tolerance("orb_eval", "strict") + aos_jax = _compute_AOs_cart(aos_data=aos_data, r_carts=r_carts) aos_debug = _compute_AOs_cart_debug(aos_data=aos_data, r_carts=r_carts) assert not np.any(np.isnan(np.asarray(aos_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(aos_debug))), "NaN detected in second argument" - np.testing.assert_allclose(aos_jax, aos_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(aos_jax, aos_debug, atol=atol, rtol=rtol) jax.clear_caches() @@ -509,20 +504,22 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_auto(): gx_auto, gy_auto, gz_auto = _compute_AOs_grad_autodiff(aos_data=aos_data, r_carts=r_carts) gx_an, gy_an, gz_an = compute_AOs_grad(aos_data=aos_data, r_carts=r_carts) + atol, rtol = get_tolerance("kinetic", "strict") assert not np.any(np.isnan(np.asarray(gx_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_auto))), "NaN detected in second argument" - np.testing.assert_allclose(gx_an, gx_auto, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv) + np.testing.assert_allclose(gx_an, gx_auto, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gy_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gy_auto))), "NaN detected in second argument" - np.testing.assert_allclose(gy_an, gy_auto, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv) + np.testing.assert_allclose(gy_an, gy_auto, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gz_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gz_auto))), "NaN detected in second argument" - np.testing.assert_allclose(gz_an, gz_auto, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv) + np.testing.assert_allclose(gz_an, gz_auto, atol=atol, rtol=rtol) jax.clear_caches() @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): """Test the grad AOs computation, comparing the JAX and debug implementations.""" # Cartesian case @@ -570,15 +567,16 @@ def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): gx_auto_cart, gy_auto_cart, gz_auto_cart = _compute_AOs_grad_autodiff(aos_data=aos_data_cart, r_carts=r_carts) gx_num_cart, gy_num_cart, gz_num_cart = _compute_AOs_grad_debug(aos_data=aos_data_cart, r_carts=r_carts) + atol, rtol = get_tolerance("kinetic", "loose") assert not np.any(np.isnan(np.asarray(gx_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_cart))), "NaN detected in second argument" - np.testing.assert_allclose(gx_auto_cart, gx_num_cart, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv) + np.testing.assert_allclose(gx_auto_cart, gx_num_cart, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gy_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gy_num_cart))), "NaN detected in second argument" - np.testing.assert_allclose(gy_auto_cart, gy_num_cart, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv) + np.testing.assert_allclose(gy_auto_cart, gy_num_cart, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gz_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gz_num_cart))), "NaN detected in second argument" - np.testing.assert_allclose(gz_auto_cart, gz_num_cart, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv) + np.testing.assert_allclose(gz_auto_cart, gz_num_cart, atol=atol, rtol=rtol) # Spherical case num_r_cart_samples = 10 @@ -634,14 +632,14 @@ def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): assert not np.any(np.isnan(np.asarray(gx_auto_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(gx_auto_sphe, gx_num_sphe, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv) + np.testing.assert_allclose(gx_auto_sphe, gx_num_sphe, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gy_auto_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gy_num_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(gy_auto_sphe, gy_num_sphe, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv) + np.testing.assert_allclose(gy_auto_sphe, gy_num_sphe, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gz_auto_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gz_num_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(gz_auto_sphe, gz_num_sphe, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv) + np.testing.assert_allclose(gz_auto_sphe, gz_num_sphe, atol=atol, rtol=rtol) # Spherical case (additional coverage) num_r_cart_samples = 2 @@ -697,17 +695,18 @@ def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): assert not np.any(np.isnan(np.asarray(gx_auto_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(gx_auto_sphe, gx_num_sphe, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv) + np.testing.assert_allclose(gx_auto_sphe, gx_num_sphe, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gy_auto_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gy_num_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(gy_auto_sphe, gy_num_sphe, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv) + np.testing.assert_allclose(gy_auto_sphe, gy_num_sphe, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gz_auto_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gz_num_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(gz_auto_sphe, gz_num_sphe, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv) + np.testing.assert_allclose(gz_auto_sphe, gz_num_sphe, atol=atol, rtol=rtol) jax.clear_caches() +@pytest.mark.numerical_diff def test_AOs_sphe_and_cart_grads_analytic_vs_numerical(): """Analytic AO gradients match numerical finite-difference implementation.""" seed = 2028 @@ -756,21 +755,16 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_numerical(): gx_num_cart, gy_num_cart, gz_num_cart = _compute_AOs_grad_debug(aos_data=aos_data, r_carts=r_carts) gx_an_cart, gy_an_cart, gz_an_cart = compute_AOs_grad(aos_data=aos_data, r_carts=r_carts) + atol, rtol = get_tolerance("kinetic", "loose") assert not np.any(np.isnan(np.asarray(gx_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_cart))), "NaN detected in second argument" - np.testing.assert_allclose( - gx_an_cart, gx_num_cart, atol=atol_numerical_vs_analytic_deriv, rtol=rtol_numerical_vs_analytic_deriv - ) + np.testing.assert_allclose(gx_an_cart, gx_num_cart, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gy_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gy_num_cart))), "NaN detected in second argument" - np.testing.assert_allclose( - gy_an_cart, gy_num_cart, atol=atol_numerical_vs_analytic_deriv, rtol=rtol_numerical_vs_analytic_deriv - ) + np.testing.assert_allclose(gy_an_cart, gy_num_cart, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gz_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gz_num_cart))), "NaN detected in second argument" - np.testing.assert_allclose( - gz_an_cart, gz_num_cart, atol=atol_numerical_vs_analytic_deriv, rtol=rtol_numerical_vs_analytic_deriv - ) + np.testing.assert_allclose(gz_an_cart, gz_num_cart, atol=atol, rtol=rtol) # Spherical case num_r_cart_samples = 3 @@ -813,19 +807,13 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_numerical(): assert not np.any(np.isnan(np.asarray(gx_an_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_sphe))), "NaN detected in second argument" - np.testing.assert_allclose( - gx_an_sphe, gx_num_sphe, atol=atol_numerical_vs_analytic_deriv, rtol=rtol_numerical_vs_analytic_deriv - ) + np.testing.assert_allclose(gx_an_sphe, gx_num_sphe, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gy_an_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gy_num_sphe))), "NaN detected in second argument" - np.testing.assert_allclose( - gy_an_sphe, gy_num_sphe, atol=atol_numerical_vs_analytic_deriv, rtol=rtol_numerical_vs_analytic_deriv - ) + np.testing.assert_allclose(gy_an_sphe, gy_num_sphe, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(gz_an_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gz_num_sphe))), "NaN detected in second argument" - np.testing.assert_allclose( - gz_an_sphe, gz_num_sphe, atol=atol_numerical_vs_analytic_deriv, rtol=rtol_numerical_vs_analytic_deriv - ) + np.testing.assert_allclose(gz_an_sphe, gz_num_sphe, atol=atol, rtol=rtol) jax.clear_caches() @@ -879,9 +867,10 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_auto(): lap_auto_cart = _compute_AOs_laplacian_autodiff(aos_data=aos_data, r_carts=r_carts) lap_an_cart = compute_AOs_laplacian(aos_data=aos_data, r_carts=r_carts) + atol, rtol = get_tolerance("kinetic", "strict") assert not np.any(np.isnan(np.asarray(lap_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_auto_cart))), "NaN detected in second argument" - np.testing.assert_allclose(lap_an_cart, lap_auto_cart, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv) + np.testing.assert_allclose(lap_an_cart, lap_auto_cart, atol=atol, rtol=rtol) # Spherical case num_r_cart_samples = 3 @@ -924,9 +913,10 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_auto(): assert not np.any(np.isnan(np.asarray(lap_an_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_auto_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(lap_an_sphe, lap_auto_sphe, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv) + np.testing.assert_allclose(lap_an_sphe, lap_auto_sphe, atol=atol, rtol=rtol) +@pytest.mark.numerical_diff def test_AOs_shpe_and_cart_laplacians_analytic_vs_numerical(): """Analytic Laplacians match numerical finite-difference implementation.""" seed = 2027 @@ -975,11 +965,10 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_numerical(): lap_num_cart = _compute_AOs_laplacian_debug(aos_data=aos_data, r_carts=r_carts) lap_an_cart = compute_AOs_laplacian(aos_data=aos_data, r_carts=r_carts) + atol, rtol = get_tolerance("kinetic", "loose") assert not np.any(np.isnan(np.asarray(lap_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" - np.testing.assert_allclose( - lap_an_cart, lap_num_cart, atol=atol_numerical_vs_analytic_deriv, rtol=rtol_numerical_vs_analytic_deriv - ) + np.testing.assert_allclose(lap_an_cart, lap_num_cart, atol=atol, rtol=rtol) # Spherical case num_r_cart_samples = 3 @@ -1022,14 +1011,13 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_numerical(): assert not np.any(np.isnan(np.asarray(lap_an_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_sphe))), "NaN detected in second argument" - np.testing.assert_allclose( - lap_an_sphe, lap_num_sphe, atol=atol_numerical_vs_analytic_deriv, rtol=rtol_numerical_vs_analytic_deriv - ) + np.testing.assert_allclose(lap_an_sphe, lap_num_sphe, atol=atol, rtol=rtol) jax.clear_caches() @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): """Test the laplacian AOs computation, comparing the JAX and debug implementations.""" # Cartesian case @@ -1077,11 +1065,10 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): lap_num_cart = _compute_AOs_laplacian_autodiff(aos_data=aos_data, r_carts=r_carts) lap_auto_cart = _compute_AOs_laplacian_debug(aos_data=aos_data, r_carts=r_carts) + atol, rtol = get_tolerance("kinetic", "loose") assert not np.any(np.isnan(np.asarray(lap_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" - np.testing.assert_allclose( - lap_auto_cart, lap_num_cart, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(lap_auto_cart, lap_num_cart, atol=atol, rtol=rtol) # Spherical cases num_r_cart_samples = 10 @@ -1133,9 +1120,7 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): assert not np.any(np.isnan(np.asarray(lap_num_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_auto_sphe))), "NaN detected in second argument" - np.testing.assert_allclose( - lap_num_sphe, lap_auto_sphe, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(lap_num_sphe, lap_auto_sphe, atol=atol, rtol=rtol) num_r_cart_samples = 2 num_R_cart_samples = 3 @@ -1186,13 +1171,12 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): assert not np.any(np.isnan(np.asarray(lap_num_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_auto_sphe))), "NaN detected in second argument" - np.testing.assert_allclose( - lap_num_sphe, lap_auto_sphe, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(lap_num_sphe, lap_auto_sphe, atol=atol, rtol=rtol) jax.clear_caches() +@pytest.mark.numerical_diff def test_overlap_matrix_cart_analytic_vs_numerical_debug(): """Cartesian AO overlap matrix from analytic formula matches numerical integration.""" centers = np.array([[-0.45, 0.0, 0.0], [0.45, 0.0, 0.0]], dtype=np.float64) @@ -1224,19 +1208,19 @@ def test_overlap_matrix_cart_analytic_vs_numerical_debug(): overlap_analytic = np.asarray(compute_overlap_matrix(aos_data=aos_data), dtype=np.float64) overlap_numerical = _compute_overlap_matrix_debug(aos_data=aos_data, num_grid_points=41, tail_tolerance=1.0e-12) + atol, rtol = get_tolerance("orb_eval", "strict") assert not np.any(np.isnan(np.asarray(overlap_analytic))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(overlap_numerical))), "NaN detected in second argument" - np.testing.assert_allclose( - overlap_analytic, overlap_numerical, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(overlap_analytic, overlap_numerical, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(overlap_analytic))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(overlap_analytic.T))), "NaN detected in second argument" - np.testing.assert_allclose(overlap_analytic, overlap_analytic.T, atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(overlap_analytic, overlap_analytic.T, atol=atol, rtol=rtol) assert np.all(np.diag(overlap_analytic) > 0.0) jax.clear_caches() +@pytest.mark.numerical_diff def test_overlap_matrix_sphe_analytic_vs_numerical_debug(): """Spherical AO overlap matrix from analytic formula matches numerical integration.""" centers = np.array([[-0.35, 0.0, 0.0], [0.35, 0.0, 0.0]], dtype=np.float64) @@ -1266,14 +1250,14 @@ def test_overlap_matrix_sphe_analytic_vs_numerical_debug(): overlap_analytic = np.asarray(compute_overlap_matrix(aos_data=aos_data), dtype=np.float64) overlap_numerical = _compute_overlap_matrix_debug(aos_data=aos_data, num_grid_points=41, tail_tolerance=1.0e-12) + atol_l, rtol_l = get_tolerance("orb_eval", "loose") + atol_s, rtol_s = get_tolerance("orb_eval", "strict") assert not np.any(np.isnan(np.asarray(overlap_analytic))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(overlap_numerical))), "NaN detected in second argument" - np.testing.assert_allclose( - overlap_analytic, overlap_numerical, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(overlap_analytic, overlap_numerical, atol=atol_l, rtol=rtol_l) assert not np.any(np.isnan(np.asarray(overlap_analytic))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(overlap_analytic.T))), "NaN detected in second argument" - np.testing.assert_allclose(overlap_analytic, overlap_analytic.T, atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(overlap_analytic, overlap_analytic.T, atol=atol_s, rtol=rtol_s) assert np.all(np.diag(overlap_analytic) > 0.0) jax.clear_caches() diff --git a/tests/test_MOs.py b/tests/test_MOs.py index 87b83e1c..4afd12ed 100755 --- a/tests/test_MOs.py +++ b/tests/test_MOs.py @@ -62,14 +62,7 @@ compute_MOs_grad, compute_MOs_laplacian, ) -from jqmc._setting import ( # noqa: E402 - atol_auto_vs_analytic_deriv, - rtol_auto_vs_analytic_deriv, - atol_auto_vs_numerical_deriv, - rtol_auto_vs_numerical_deriv, - atol_debug_vs_production, - rtol_debug_vs_production, -) +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.structure import Structure_data # noqa: E402 # JAX float64 @@ -132,9 +125,10 @@ def test_MOs_comparing_jax_and_debug_implemenetations(): mo_ans_all_debug = _compute_MOs_debug(mos_data=mos_data, r_carts=r_carts) + atol, rtol = get_tolerance("orb_eval", "strict") assert not np.any(np.isnan(np.asarray(mo_ans_all_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_ans_all_jax))), "NaN detected in second argument" - np.testing.assert_allclose(mo_ans_all_debug, mo_ans_all_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(mo_ans_all_debug, mo_ans_all_jax, atol=atol, rtol=rtol) num_el = 10 num_mo = 5 @@ -190,12 +184,13 @@ def test_MOs_comparing_jax_and_debug_implemenetations(): assert not np.any(np.isnan(np.asarray(mo_ans_all_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_ans_all_jax))), "NaN detected in second argument" - np.testing.assert_allclose(mo_ans_all_debug, mo_ans_all_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(mo_ans_all_debug, mo_ans_all_jax, atol=atol, rtol=rtol) jax.clear_caches() @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff def test_MOs_comparing_auto_and_numerical_grads(): """Test the MO gradient computation, comparing JAX and debug implementations.""" num_el = 10 @@ -254,22 +249,17 @@ def test_MOs_comparing_auto_and_numerical_grads(): mo_matrix_grad_z_numerical, ) = _compute_MOs_grad_autodiff(mos_data=mos_data, r_carts=r_carts) + atol, rtol = get_tolerance("kinetic", "loose") assert not np.any(np.isnan(np.asarray(mo_matrix_grad_x_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_grad_x_numerical))), "NaN detected in second argument" - np.testing.assert_allclose( - mo_matrix_grad_x_auto, mo_matrix_grad_x_numerical, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(mo_matrix_grad_x_auto, mo_matrix_grad_x_numerical, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(mo_matrix_grad_y_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_grad_y_numerical))), "NaN detected in second argument" - np.testing.assert_allclose( - mo_matrix_grad_y_auto, mo_matrix_grad_y_numerical, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(mo_matrix_grad_y_auto, mo_matrix_grad_y_numerical, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(mo_matrix_grad_z_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_grad_z_numerical))), "NaN detected in second argument" - np.testing.assert_allclose( - mo_matrix_grad_z_auto, mo_matrix_grad_z_numerical, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(mo_matrix_grad_z_auto, mo_matrix_grad_z_numerical, atol=atol, rtol=rtol) num_el = 10 num_mo = 5 @@ -329,24 +319,19 @@ def test_MOs_comparing_auto_and_numerical_grads(): assert not np.any(np.isnan(np.asarray(mo_matrix_grad_x_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_grad_x_numerical))), "NaN detected in second argument" - np.testing.assert_allclose( - mo_matrix_grad_x_auto, mo_matrix_grad_x_numerical, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(mo_matrix_grad_x_auto, mo_matrix_grad_x_numerical, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(mo_matrix_grad_y_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_grad_y_numerical))), "NaN detected in second argument" - np.testing.assert_allclose( - mo_matrix_grad_y_auto, mo_matrix_grad_y_numerical, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(mo_matrix_grad_y_auto, mo_matrix_grad_y_numerical, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(mo_matrix_grad_z_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_grad_z_numerical))), "NaN detected in second argument" - np.testing.assert_allclose( - mo_matrix_grad_z_auto, mo_matrix_grad_z_numerical, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(mo_matrix_grad_z_auto, mo_matrix_grad_z_numerical, atol=atol, rtol=rtol) jax.clear_caches() @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff def test_MOs_comparing_auto_and_numerical_laplacians(): """Test the MO Laplacian computation, comparing JAX and debug implementations.""" num_el = 10 @@ -401,13 +386,14 @@ def test_MOs_comparing_auto_and_numerical_laplacians(): mo_matrix_laplacian_auto = _compute_MOs_laplacian_autodiff(mos_data=mos_data, r_carts=r_carts) + atol, rtol = get_tolerance("kinetic", "loose") assert not np.any(np.isnan(np.asarray(mo_matrix_laplacian_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_laplacian_numerical))), "NaN detected in second argument" np.testing.assert_allclose( mo_matrix_laplacian_auto, mo_matrix_laplacian_numerical, - atol=atol_auto_vs_numerical_deriv, - rtol=rtol_auto_vs_numerical_deriv, + atol=atol, + rtol=rtol, ) jax.clear_caches() @@ -466,15 +452,16 @@ def test_MOs_comparing_analytic_and_auto_grads(): grad_x_auto, grad_y_auto, grad_z_auto = _compute_MOs_grad_autodiff(mos_data=mos_data, r_carts=r_carts) + atol, rtol = get_tolerance("kinetic", "strict") assert not np.any(np.isnan(np.asarray(grad_x_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_x_auto))), "NaN detected in second argument" - np.testing.assert_allclose(grad_x_an, grad_x_auto, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv) + np.testing.assert_allclose(grad_x_an, grad_x_auto, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(grad_y_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_y_auto))), "NaN detected in second argument" - np.testing.assert_allclose(grad_y_an, grad_y_auto, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv) + np.testing.assert_allclose(grad_y_an, grad_y_auto, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(grad_z_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_z_auto))), "NaN detected in second argument" - np.testing.assert_allclose(grad_z_an, grad_z_auto, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv) + np.testing.assert_allclose(grad_z_an, grad_z_auto, atol=atol, rtol=rtol) jax.clear_caches() @@ -532,9 +519,10 @@ def test_MOs_comparing_analytic_and_auto_laplacians(): mo_lap_auto = _compute_MOs_laplacian_autodiff(mos_data=mos_data, r_carts=r_carts) + atol, rtol = get_tolerance("kinetic", "strict") assert not np.any(np.isnan(np.asarray(mo_lap_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_lap_auto))), "NaN detected in second argument" - np.testing.assert_allclose(mo_lap_an, mo_lap_auto, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv) + np.testing.assert_allclose(mo_lap_an, mo_lap_auto, atol=atol, rtol=rtol) jax.clear_caches() @@ -602,9 +590,10 @@ def test_MOs_sphe_to_cart(): mo_sphe = compute_MOs(mos_data=mos_sphe, r_carts=r_carts) mo_cart = compute_MOs(mos_data=mos_cart, r_carts=r_carts) + atol, rtol = get_tolerance("orb_eval", "strict") assert not np.any(np.isnan(np.asarray(mo_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(mo_cart, mo_sphe, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(mo_cart, mo_sphe, atol=atol, rtol=rtol) grad_sphe = compute_MOs_grad(mos_data=mos_sphe, r_carts=r_carts) grad_cart = compute_MOs_grad(mos_data=mos_cart, r_carts=r_carts) @@ -612,14 +601,14 @@ def test_MOs_sphe_to_cart(): for g_cart, g_sphe in zip(grad_cart, grad_sphe, strict=True): assert not np.any(np.isnan(np.asarray(g_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(g_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(g_cart, g_sphe, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(g_cart, g_sphe, atol=atol, rtol=rtol) lap_sphe = compute_MOs_laplacian(mos_data=mos_sphe, r_carts=r_carts) lap_cart = compute_MOs_laplacian(mos_data=mos_cart, r_carts=r_carts) assert not np.any(np.isnan(np.asarray(lap_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(lap_cart, lap_sphe, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(lap_cart, lap_sphe, atol=atol, rtol=rtol) jax.clear_caches() @@ -703,9 +692,10 @@ def test_MOs_cart_to_sphe(): mo_cart = compute_MOs(mos_data=mos_cart, r_carts=r_carts) mo_sphe = compute_MOs(mos_data=mos_sphe, r_carts=r_carts) + atol, rtol = get_tolerance("orb_eval", "strict") assert not np.any(np.isnan(np.asarray(mo_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_cart))), "NaN detected in second argument" - np.testing.assert_allclose(mo_sphe, mo_cart, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(mo_sphe, mo_cart, atol=atol, rtol=rtol) grad_cart = compute_MOs_grad(mos_data=mos_cart, r_carts=r_carts) grad_sphe = compute_MOs_grad(mos_data=mos_sphe, r_carts=r_carts) @@ -713,14 +703,14 @@ def test_MOs_cart_to_sphe(): for g_cart, g_sphe in zip(grad_cart, grad_sphe, strict=True): assert not np.any(np.isnan(np.asarray(g_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(g_cart))), "NaN detected in second argument" - np.testing.assert_allclose(g_sphe, g_cart, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(g_sphe, g_cart, atol=atol, rtol=rtol) lap_cart = compute_MOs_laplacian(mos_data=mos_cart, r_carts=r_carts) lap_sphe = compute_MOs_laplacian(mos_data=mos_sphe, r_carts=r_carts) assert not np.any(np.isnan(np.asarray(lap_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_cart))), "NaN detected in second argument" - np.testing.assert_allclose(lap_sphe, lap_cart, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(lap_sphe, lap_cart, atol=atol, rtol=rtol) jax.clear_caches() diff --git a/tests/test_ao_basis_optimization.py b/tests/test_ao_basis_optimization.py index 967ac59a..586f3688 100644 --- a/tests/test_ao_basis_optimization.py +++ b/tests/test_ao_basis_optimization.py @@ -21,6 +21,7 @@ Jastrow_three_body_data, compute_Jastrow_three_body, ) +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.molecular_orbital import MOs_data # noqa: E402 from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 from jqmc.wavefunction import ( # noqa: E402 @@ -128,9 +129,19 @@ def test_j3_with_updated_ao_exponents(): new_exp = j3.ao_exponents * 1.1 j3_new = j3.with_updated_ao_exponents(new_exp) - npt.assert_allclose(np.array(j3_new.ao_exponents), np.array(new_exp), rtol=1e-14) + npt.assert_allclose( + np.array(j3_new.ao_exponents), + np.array(new_exp), + atol=get_tolerance("orb_eval", "strict")[0], + rtol=get_tolerance("orb_eval", "strict")[1], + ) # Original should be unchanged - npt.assert_allclose(np.array(j3.ao_exponents), np.array(aos_data.exponents), rtol=1e-14) + npt.assert_allclose( + np.array(j3.ao_exponents), + np.array(aos_data.exponents), + atol=get_tolerance("orb_eval", "strict")[0], + rtol=get_tolerance("orb_eval", "strict")[1], + ) # ============================================================ @@ -152,8 +163,18 @@ def test_geminal_ao_properties(): geminal_ao = Geminal_data.convert_from_MOs_to_AOs(geminal_mo_data) exp_up_ao = geminal_ao.ao_exponents_up exp_dn_ao = geminal_ao.ao_exponents_dn - npt.assert_allclose(np.array(exp_up), np.array(exp_up_ao), rtol=1e-14) - npt.assert_allclose(np.array(exp_dn), np.array(exp_dn_ao), rtol=1e-14) + npt.assert_allclose( + np.array(exp_up), + np.array(exp_up_ao), + atol=get_tolerance("orb_eval", "strict")[0], + rtol=get_tolerance("orb_eval", "strict")[1], + ) + npt.assert_allclose( + np.array(exp_dn), + np.array(exp_dn_ao), + atol=get_tolerance("orb_eval", "strict")[0], + rtol=get_tolerance("orb_eval", "strict")[1], + ) # ============================================================ @@ -170,8 +191,18 @@ def test_geminal_with_updated_ao_exponents(): new_exp_dn = geminal_ao.ao_exponents_dn * 1.1 geminal_new = geminal_ao.with_updated_ao_exponents(new_exp_up, new_exp_dn) - npt.assert_allclose(np.array(geminal_new.ao_exponents_up), np.array(new_exp_up), rtol=1e-14) - npt.assert_allclose(np.array(geminal_new.ao_exponents_dn), np.array(new_exp_dn), rtol=1e-14) + npt.assert_allclose( + np.array(geminal_new.ao_exponents_up), + np.array(new_exp_up), + atol=get_tolerance("orb_eval", "strict")[0], + rtol=get_tolerance("orb_eval", "strict")[1], + ) + npt.assert_allclose( + np.array(geminal_new.ao_exponents_dn), + np.array(new_exp_dn), + atol=get_tolerance("orb_eval", "strict")[0], + rtol=get_tolerance("orb_eval", "strict")[1], + ) # Lambda matrix should be unchanged npt.assert_array_equal(np.array(geminal_new.lambda_matrix), np.array(geminal_ao.lambda_matrix)) @@ -182,6 +213,7 @@ def test_geminal_with_updated_ao_exponents(): @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff @pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvdz_cart.h5", "H2_ae_ccpvdz_sphe.h5"]) def test_j3_exponent_gradient_finite_diff(trexio_file): """Verify that jax.grad of J3 w.r.t. exponents matches finite differences.""" @@ -220,6 +252,7 @@ def j3_value(exponents): @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff @pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvdz_cart.h5", "H2_ae_ccpvdz_sphe.h5"]) def test_j3_coefficient_gradient_finite_diff(trexio_file): """Verify that jax.grad of J3 w.r.t. coefficients matches finite differences.""" @@ -256,6 +289,7 @@ def j3_value(coefficients): @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff def test_geminal_exponent_gradient_finite_diff(): """Verify that jax.grad of Geminal det w.r.t. exponents matches finite differences.""" structure_data, _, _, _, geminal_mo_data, coulomb_potential_data = _load_trexio("H2_ae_ccpvdz_cart.h5") @@ -333,7 +367,12 @@ def test_get_variational_blocks_basis_flags(): # symmetrize_metric should be set and be idempotent on the current values assert j3_exp_block.symmetrize_metric is not None symmetrized = j3_exp_block.symmetrize_metric(np.asarray(j3_exp_block.values)) - npt.assert_allclose(symmetrized, np.asarray(aos_data.exponents), rtol=1e-14) + npt.assert_allclose( + symmetrized, + np.asarray(aos_data.exponents), + atol=get_tolerance("orb_eval", "strict")[0], + rtol=get_tolerance("orb_eval", "strict")[1], + ) # ============================================================ @@ -363,7 +402,8 @@ def test_apply_block_update_j3_basis(): npt.assert_allclose( np.array(jastrow_new.jastrow_three_body_data.ao_exponents), new_exp, - rtol=1e-14, + atol=get_tolerance("orb_eval", "strict")[0], + rtol=get_tolerance("orb_eval", "strict")[1], ) @@ -387,8 +427,18 @@ def test_apply_block_update_geminal_basis(): size=int(new_exp.size), ) geminal_new = geminal_ao.apply_block_update(block) - npt.assert_allclose(np.array(geminal_new.ao_exponents_up), new_exp_up, rtol=1e-14) - npt.assert_allclose(np.array(geminal_new.ao_exponents_dn), new_exp_dn, rtol=1e-14) + npt.assert_allclose( + np.array(geminal_new.ao_exponents_up), + new_exp_up, + atol=get_tolerance("orb_eval", "strict")[0], + rtol=get_tolerance("orb_eval", "strict")[1], + ) + npt.assert_allclose( + np.array(geminal_new.ao_exponents_dn), + new_exp_dn, + atol=get_tolerance("orb_eval", "strict")[0], + rtol=get_tolerance("orb_eval", "strict")[1], + ) # ============================================================ @@ -560,7 +610,9 @@ def test_shell_symmetrize_j3_basis(): # apply_block_update shell-averages the perturbed values spm = ShellPrimMap.from_aos_data(aos_data) expected = spm.symmetrize(perturbed) - npt.assert_allclose(result, expected, rtol=1e-14) + npt.assert_allclose( + result, expected, atol=get_tolerance("orb_eval", "strict")[0], rtol=get_tolerance("orb_eval", "strict")[1] + ) def test_shell_symmetrize_geminal_basis(): @@ -603,7 +655,9 @@ def test_shell_symmetrize_geminal_basis(): ShellPrimMap.from_aos_data(_get_aos_data(geminal_ao.orb_data_dn_spin)), ) expected = spm.symmetrize(perturbed) - npt.assert_allclose(result, expected, rtol=1e-14) + npt.assert_allclose( + result, expected, atol=get_tolerance("orb_eval", "strict")[0], rtol=get_tolerance("orb_eval", "strict")[1] + ) def test_shell_symmetrize_metric_averages_sn(): diff --git a/tests/test_comparison_with_turborvb_AE.py b/tests/test_comparison_with_turborvb_AE.py index 02dafcf0..1c5b426f 100755 --- a/tests/test_comparison_with_turborvb_AE.py +++ b/tests/test_comparison_with_turborvb_AE.py @@ -39,6 +39,7 @@ import jax import numpy as np +import pytest # Add the project root directory to sys.path to allow executing this script directly # This is necessary because relative imports (e.g. 'from ..jqmc') are not allowed @@ -57,6 +58,8 @@ jax.config.update("jax_enable_x64", True) jax.config.update("jax_traceback_filtering", "off") +pytestmark = pytest.mark.external_reference + def test_comparison_with_TurboRVB_wo_Jastrow_AE(): """Test comparison with the corresponding all-electron TurboRVB calculation without Jastrow factor.""" diff --git a/tests/test_comparison_with_turborvb_ECP.py b/tests/test_comparison_with_turborvb_ECP.py index a066b42c..d79cc35e 100755 --- a/tests/test_comparison_with_turborvb_ECP.py +++ b/tests/test_comparison_with_turborvb_ECP.py @@ -39,6 +39,7 @@ import jax import numpy as np +import pytest from jax import numpy as jnp # Add the project root directory to sys.path to allow executing this script directly @@ -65,6 +66,8 @@ jax.config.update("jax_enable_x64", True) jax.config.update("jax_traceback_filtering", "off") +pytestmark = pytest.mark.external_reference + Nv = 6 NN = 1 diff --git a/tests/test_determinant.py b/tests/test_determinant.py index e3d2843d..dc1cce5c 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -46,16 +46,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import ( # noqa: E402 - atol_auto_vs_analytic_deriv, - atol_auto_vs_numerical_deriv, - atol_consistency, - atol_debug_vs_production, - rtol_auto_vs_analytic_deriv, - rtol_auto_vs_numerical_deriv, - rtol_consistency, - rtol_debug_vs_production, -) +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.atomic_orbital import AOs_sphe_data, compute_overlap_matrix # noqa: E402 from jqmc.determinant import ( # noqa: E402 Geminal_data, @@ -165,6 +156,9 @@ def test_convert_from_MOs_to_AOs_closed_shell(trexio_file: str): r_up_carts = np.array(r_up_carts).reshape(-1, 3) r_dn_carts = np.array(r_dn_carts).reshape(-1, 3) + atol_g, rtol_g = get_tolerance("geminal", "strict") + atol_d, rtol_d = get_tolerance("determinant", "strict") + geminal_mo_debug = _compute_geminal_all_elements_debug( geminal_data=geminal_mo_data, r_up_carts=r_up_carts, @@ -179,7 +173,7 @@ def test_convert_from_MOs_to_AOs_closed_shell(trexio_file: str): assert not np.any(np.isnan(np.asarray(geminal_mo_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(geminal_mo_jax))), "NaN detected in second argument" - np.testing.assert_allclose(geminal_mo_debug, geminal_mo_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(geminal_mo_debug, geminal_mo_jax, atol=atol_g, rtol=rtol_g) geminal_mo = geminal_mo_jax @@ -220,14 +214,14 @@ def test_convert_from_MOs_to_AOs_closed_shell(trexio_file: str): assert not np.any(np.isnan(np.asarray(geminal_ao_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(geminal_ao_jax))), "NaN detected in second argument" - np.testing.assert_allclose(geminal_ao_debug, geminal_ao_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(geminal_ao_debug, geminal_ao_jax, atol=atol_g, rtol=rtol_g) geminal_ao = geminal_ao_jax # check if geminals with AO and MO representations are consistent assert not np.any(np.isnan(np.asarray(geminal_ao))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(geminal_mo))), "NaN detected in second argument" - np.testing.assert_allclose(geminal_ao, geminal_mo, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(geminal_ao, geminal_mo, atol=atol_g, rtol=rtol_g) det_geminal_mo_debug = _compute_det_geminal_all_elements_debug( geminal_data=geminal_mo_data, @@ -243,9 +237,7 @@ def test_convert_from_MOs_to_AOs_closed_shell(trexio_file: str): assert not np.any(np.isnan(np.asarray(det_geminal_mo_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(det_geminal_mo_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - det_geminal_mo_debug, det_geminal_mo_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(det_geminal_mo_debug, det_geminal_mo_jax, atol=atol_d, rtol=rtol_d) det_geminal_mo = det_geminal_mo_jax @@ -263,14 +255,12 @@ def test_convert_from_MOs_to_AOs_closed_shell(trexio_file: str): assert not np.any(np.isnan(np.asarray(det_geminal_ao_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(det_geminal_ao_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - det_geminal_ao_debug, det_geminal_ao_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(det_geminal_ao_debug, det_geminal_ao_jax, atol=atol_d, rtol=rtol_d) det_geminal_ao = det_geminal_ao_jax assert not np.any(np.isnan(np.asarray(det_geminal_ao))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(det_geminal_mo))), "NaN detected in second argument" - np.testing.assert_allclose(det_geminal_ao, det_geminal_mo, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(det_geminal_ao, det_geminal_mo, atol=atol_d, rtol=rtol_d) jax.clear_caches() @@ -319,6 +309,7 @@ def _build_sphe_aos_l_le6(rng: np.random.Generator) -> AOs_sphe_data: def test_geminal_sphe_to_cart_AOs_data(): """Round-trip AOs l<=6: spherical→Cartesian keeps geminal values/grads.""" + atol_c, rtol_c = get_tolerance("geminal", "strict") rng = np.random.default_rng(321) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -342,20 +333,21 @@ def test_geminal_sphe_to_cart_AOs_data(): G_cart = compute_geminal_all_elements(geminal_cart, r_up_carts, r_dn_carts) assert not np.any(np.isnan(np.asarray(np.asarray(G_sph)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(G_cart)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(G_sph), np.asarray(G_cart), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(G_sph), np.asarray(G_cart), atol=atol_c, rtol=rtol_c) grads_sph = compute_grads_and_laplacian_ln_Det(geminal_sph, r_up_carts, r_dn_carts) grads_cart = compute_grads_and_laplacian_ln_Det(geminal_cart, r_up_carts, r_dn_carts) for sph, cart in zip(grads_sph, grads_cart, strict=True): assert not np.any(np.isnan(np.asarray(np.asarray(sph)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(cart)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(sph), np.asarray(cart), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(sph), np.asarray(cart), atol=atol_c, rtol=rtol_c) jax.clear_caches() def test_geminal_cart_to_sphe_AOs_data(): """Round-trip AOs l<=6: Cartesian→spherical keeps geminal values/grads.""" + atol_c, rtol_c = get_tolerance("geminal", "strict") rng = np.random.default_rng(654) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -381,20 +373,21 @@ def test_geminal_cart_to_sphe_AOs_data(): G_sph = compute_geminal_all_elements(geminal_cart_to_sph, r_up_carts, r_dn_carts) assert not np.any(np.isnan(np.asarray(np.asarray(G_cart)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(G_sph)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(G_cart), np.asarray(G_sph), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(G_cart), np.asarray(G_sph), atol=atol_c, rtol=rtol_c) grads_cart = compute_grads_and_laplacian_ln_Det(geminal_cart, r_up_carts, r_dn_carts) grads_sph = compute_grads_and_laplacian_ln_Det(geminal_cart_to_sph, r_up_carts, r_dn_carts) for cart, sph in zip(grads_cart, grads_sph, strict=True): assert not np.any(np.isnan(np.asarray(np.asarray(cart)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(sph)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(cart), np.asarray(sph), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(cart), np.asarray(sph), atol=atol_c, rtol=rtol_c) jax.clear_caches() def test_geminal_sphe_to_cart_MOs_data(): """Round-trip MOs built on l<=6 AOs: spherical→Cartesian keeps geminal values/grads.""" + atol_c, rtol_c = get_tolerance("geminal", "strict") rng = np.random.default_rng(777) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -422,20 +415,21 @@ def test_geminal_sphe_to_cart_MOs_data(): G_cart = compute_geminal_all_elements(geminal_cart, r_up_carts, r_dn_carts) assert not np.any(np.isnan(np.asarray(np.asarray(G_sph)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(G_cart)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(G_sph), np.asarray(G_cart), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(G_sph), np.asarray(G_cart), atol=atol_c, rtol=rtol_c) grads_sph = compute_grads_and_laplacian_ln_Det(geminal_sph, r_up_carts, r_dn_carts) grads_cart = compute_grads_and_laplacian_ln_Det(geminal_cart, r_up_carts, r_dn_carts) for sph, cart in zip(grads_sph, grads_cart, strict=True): assert not np.any(np.isnan(np.asarray(np.asarray(sph)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(cart)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(sph), np.asarray(cart), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(sph), np.asarray(cart), atol=atol_c, rtol=rtol_c) jax.clear_caches() def test_geminal_cart_to_sphe_MOs_data(): """Round-trip MOs l<=6: Cartesian→spherical keeps geminal values/grads.""" + atol_c, rtol_c = get_tolerance("geminal", "strict") rng = np.random.default_rng(888) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -465,14 +459,14 @@ def test_geminal_cart_to_sphe_MOs_data(): G_sph = compute_geminal_all_elements(geminal_cart_to_sph, r_up_carts, r_dn_carts) assert not np.any(np.isnan(np.asarray(np.asarray(G_cart)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(G_sph)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(G_cart), np.asarray(G_sph), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(G_cart), np.asarray(G_sph), atol=atol_c, rtol=rtol_c) grads_cart = compute_grads_and_laplacian_ln_Det(geminal_cart, r_up_carts, r_dn_carts) grads_sph = compute_grads_and_laplacian_ln_Det(geminal_cart_to_sph, r_up_carts, r_dn_carts) for cart, sph in zip(grads_cart, grads_sph, strict=True): assert not np.any(np.isnan(np.asarray(np.asarray(cart)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(sph)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(cart), np.asarray(sph), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(cart), np.asarray(sph), atol=atol_c, rtol=rtol_c) jax.clear_caches() @@ -500,6 +494,7 @@ def _build_small_sphe_aos_for_conversion() -> AOs_sphe_data: def test_convert_from_AOs_to_MOs_full_projection_closed_shell(): """AO->MO (all eigenvectors) followed by MO->AO recovers the AO lambda matrix.""" + atol_c, rtol_c = get_tolerance("determinant", "strict") rng = np.random.default_rng(1234) aos_data = _build_small_sphe_aos_for_conversion() aos_data.sanity_check() @@ -526,13 +521,14 @@ def test_convert_from_AOs_to_MOs_full_projection_closed_shell(): np.testing.assert_allclose( np.asarray(geminal_ao_back.lambda_matrix), np.asarray(geminal_ao.lambda_matrix), - atol=atol_consistency, - rtol=rtol_consistency, + atol=atol_c, + rtol=rtol_c, ) def test_convert_from_AOs_to_MOs_full_projection_open_shell(): """AO->MO (all eigenvectors) round-trip recovers AO lambda matrix for open-shell case.""" + atol_c, rtol_c = get_tolerance("determinant", "strict") rng = np.random.default_rng(1334) aos_data = _build_small_sphe_aos_for_conversion() aos_data.sanity_check() @@ -561,8 +557,8 @@ def test_convert_from_AOs_to_MOs_full_projection_open_shell(): np.testing.assert_allclose( np.asarray(geminal_ao_back.lambda_matrix), np.asarray(geminal_ao.lambda_matrix), - atol=atol_consistency, - rtol=rtol_consistency, + atol=atol_c, + rtol=rtol_c, ) @@ -633,6 +629,7 @@ def test_convert_from_AOs_to_MOs_truncated_mode_open_shell(): def test_apply_ao_projected_paired_update_and_reproject_fixed_num_dn(): """AO-corrected paired update is applied then reprojected with fixed N=num_electron_dn.""" + atol_c, rtol_c = get_tolerance("determinant", "strict") rng = np.random.default_rng(97531) aos_data = _build_small_sphe_aos_for_conversion() aos_data.sanity_check() @@ -700,8 +697,8 @@ def test_apply_ao_projected_paired_update_and_reproject_fixed_num_dn(): np.testing.assert_allclose( np.asarray(actual.lambda_matrix), np.asarray(expected.lambda_matrix), - atol=atol_consistency, - rtol=rtol_consistency, + atol=atol_c, + rtol=rtol_c, ) @@ -809,18 +806,19 @@ def test_grads_and_laplacian_fast_update(trexio_file: str): r_dn_carts=r_dn_carts, ) + atol, rtol = get_tolerance("kinetic", "strict") assert not np.any(np.isnan(np.asarray(grad_up_fast))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_up_debug))), "NaN detected in second argument" - np.testing.assert_allclose(grad_up_fast, grad_up_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(grad_up_fast, grad_up_debug, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(grad_dn_fast))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_dn_debug))), "NaN detected in second argument" - np.testing.assert_allclose(grad_dn_fast, grad_dn_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(grad_dn_fast, grad_dn_debug, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(lap_up_fast))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_up_debug))), "NaN detected in second argument" - np.testing.assert_allclose(lap_up_fast, lap_up_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(lap_up_fast, lap_up_debug, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(lap_dn_fast))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_dn_debug))), "NaN detected in second argument" - np.testing.assert_allclose(lap_dn_fast, lap_dn_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(lap_dn_fast, lap_dn_debug, atol=atol, rtol=rtol) @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) @@ -906,9 +904,10 @@ def test_comparing_AS_regularization(trexio_file: str): R_AS_jax = compute_AS_regularization_factor(geminal_data=geminal_mo_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) + atol, rtol = get_tolerance("determinant", "strict") assert not np.any(np.isnan(np.asarray(R_AS_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(R_AS_jax))), "NaN detected in second argument" - np.testing.assert_allclose(R_AS_debug, R_AS_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(R_AS_debug, R_AS_jax, atol=atol, rtol=rtol) jax.clear_caches() @@ -1016,14 +1015,15 @@ def test_one_row_or_one_column_update(trexio_file: str): ) # --- Numerical consistency asserts (no shape checks) --- + atol, rtol = get_tolerance("geminal", "strict") # up-one-row must equal the i-th row of the full geminal assert not np.any(np.isnan(np.asarray(np.asarray(geminal_mo_up_one_row).ravel()))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(geminal_mo[i_up, :])))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(geminal_mo_up_one_row).ravel(), np.asarray(geminal_mo[i_up, :]), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) # dn-one-column must equal the j-th *paired* column of the full geminal @@ -1032,11 +1032,12 @@ def test_one_row_or_one_column_update(trexio_file: str): np.testing.assert_allclose( np.asarray(geminal_mo_dn_one_column).ravel(), np.asarray(geminal_mo[:, j_dn]), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) +@pytest.mark.numerical_diff @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) def test_numerial_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): """Test the numerical and automatic gradients of the logarithm of the determinant of the geminal wave function.""" @@ -1151,37 +1152,38 @@ def test_numerial_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): r_dn_carts=r_dn_carts, ) + atol, rtol = get_tolerance("kinetic", "loose") assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_numerical)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(grad_ln_D_up_numerical), np.asarray(grad_ln_D_up_auto), - atol=atol_auto_vs_numerical_deriv, - rtol=rtol_auto_vs_numerical_deriv, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_dn_numerical)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(grad_ln_D_dn_numerical), np.asarray(grad_ln_D_dn_auto), - atol=atol_auto_vs_numerical_deriv, - rtol=rtol_auto_vs_numerical_deriv, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_up_numerical)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_ln_D_up_numerical), np.asarray(lap_ln_D_up_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol, + atol=atol, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_dn_numerical)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_ln_D_dn_numerical), np.asarray(lap_ln_D_dn_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol, + atol=atol, ) jax.clear_caches() @@ -1282,37 +1284,38 @@ def test_analytic_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): r_dn_carts=r_dn_carts, ) + atol, rtol = get_tolerance("kinetic", "strict") assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_analytic)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(grad_ln_D_up_analytic), np.asarray(grad_ln_D_up_auto), - atol=atol_auto_vs_analytic_deriv, - rtol=rtol_auto_vs_analytic_deriv, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_dn_analytic)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(grad_ln_D_dn_analytic), np.asarray(grad_ln_D_dn_auto), - atol=atol_auto_vs_analytic_deriv, - rtol=rtol_auto_vs_analytic_deriv, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_up_analytic)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_ln_D_up_analytic), np.asarray(lap_ln_D_up_auto), - atol=atol_auto_vs_analytic_deriv, - rtol=rtol_auto_vs_analytic_deriv, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_dn_analytic)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_ln_D_dn_analytic), np.asarray(lap_ln_D_dn_auto), - atol=atol_auto_vs_analytic_deriv, - rtol=rtol_auto_vs_analytic_deriv, + atol=atol, + rtol=rtol, ) jax.clear_caches() @@ -1402,18 +1405,16 @@ def test_ratio_determinant_rank1_update(pattern: str): new_r_dn_carts_arr=new_r_dn_carts_arr, ) + atol, rtol = get_tolerance("determinant", "strict") + atol_c, rtol_c = atol, rtol assert not np.any(np.isnan(np.asarray(np.asarray(ratio_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(ratio_rank1)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(ratio_debug), np.asarray(ratio_rank1), atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(np.asarray(ratio_debug), np.asarray(ratio_rank1), atol=atol, rtol=rtol) if pattern == "none_moved": assert not np.any(np.isnan(np.asarray(np.asarray(ratio_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.ones_like(np.asarray(ratio_debug))))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(ratio_debug), np.ones_like(np.asarray(ratio_debug)), atol=atol_consistency, rtol=rtol_consistency - ) + np.testing.assert_allclose(np.asarray(ratio_debug), np.ones_like(np.asarray(ratio_debug)), atol=atol_c, rtol=rtol_c) jax.clear_caches() @@ -1429,6 +1430,7 @@ def test_compute_ln_det_geminal_all_elements_fast_forward(trexio_file): n_up = geminal_data.num_electron_up n_dn = geminal_data.num_electron_dn + atol, rtol = get_tolerance("determinant", "strict") for _ in range(10): r_up = jnp.array(rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) @@ -1443,8 +1445,8 @@ def test_compute_ln_det_geminal_all_elements_fast_forward(trexio_file): np.testing.assert_allclose( val_fast, val_ref, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, err_msg=f"Forward mismatch: fast={val_fast:.15f}, ref={val_ref:.15f}", ) @@ -1465,6 +1467,7 @@ def test_compute_ln_det_geminal_all_elements_fast_backward(trexio_file): grad_ref_fn = jax.grad(compute_ln_det_geminal_all_elements, argnums=0) grad_fast_fn = jax.grad(compute_ln_det_geminal_all_elements_fast, argnums=0) + atol, rtol = get_tolerance("determinant", "strict") for _ in range(10): r_up = jnp.array(rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) @@ -1478,8 +1481,8 @@ def test_compute_ln_det_geminal_all_elements_fast_backward(trexio_file): lambda a, b: np.testing.assert_allclose( np.asarray(a), np.asarray(b), - rtol=rtol_debug_vs_production, - atol=atol_debug_vs_production, + rtol=rtol, + atol=atol, err_msg="Backward mismatch in compute_ln_det_geminal_all_elements_fast", ), grad_ref, diff --git a/tests/test_ecps.py b/tests/test_ecps.py index bae35e23..c546aaee 100755 --- a/tests/test_ecps.py +++ b/tests/test_ecps.py @@ -45,10 +45,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import ( # noqa: E402 - atol_debug_vs_production, - rtol_debug_vs_production, -) +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.coulomb_potential import ( # noqa: E402 _compute_bare_coulomb_potential_debug, _compute_bare_coulomb_potential_el_ion_element_wise_debug, @@ -108,6 +105,7 @@ @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5"]) def test_debug_and_jax_bare_coulomb(trexio_file: str): """Test the bare coulomb potential computation.""" + atol, rtol = get_tolerance("coulomb", "strict") ( _, _, @@ -159,12 +157,13 @@ def test_debug_and_jax_bare_coulomb(trexio_file: str): # print(f"vpot_bare_debug = {vpot_bare_debug}") assert not np.any(np.isnan(np.asarray(vpot_bare_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(vpot_bare_debug))), "NaN detected in second argument" - np.testing.assert_allclose(vpot_bare_jax, vpot_bare_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(vpot_bare_jax, vpot_bare_debug, atol=atol, rtol=rtol) @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5"]) def test_debug_and_jax_ecp_local(trexio_file: str): """Test the local ECP potential computation.""" + atol, rtol = get_tolerance("coulomb", "strict") ( _, _, @@ -214,9 +213,7 @@ def test_debug_and_jax_ecp_local(trexio_file: str): assert not np.any(np.isnan(np.asarray(vpot_ecp_local_full_NN_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(vpot_ecp_local_full_NN_debug))), "NaN detected in second argument" - np.testing.assert_allclose( - vpot_ecp_local_full_NN_jax, vpot_ecp_local_full_NN_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(vpot_ecp_local_full_NN_jax, vpot_ecp_local_full_NN_debug, atol=atol, rtol=rtol) @pytest.mark.activate_if_skip_heavy @@ -224,6 +221,7 @@ def test_debug_and_jax_ecp_local(trexio_file: str): @pytest.mark.parametrize("alpha, beta, gamma", angle_params) def test_debug_and_jax_ecp_non_local_full_NN(Nv, alpha, beta, gamma): """Test the non-local ECP potential computation with the full neibohrs.""" + atol, rtol = get_tolerance("coulomb", "strict") ( structure_data, _, @@ -314,9 +312,7 @@ def test_debug_and_jax_ecp_non_local_full_NN(Nv, alpha, beta, gamma): assert not np.any(np.isnan(np.asarray(sum_V_nonlocal_full_NN_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(sum_V_nonlocal_full_NN_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - sum_V_nonlocal_full_NN_debug, sum_V_nonlocal_full_NN_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(sum_V_nonlocal_full_NN_debug, sum_V_nonlocal_full_NN_jax, atol=atol, rtol=rtol) mesh_non_local_r_up_carts_max_full_NN_jax = mesh_non_local_ecp_part_r_up_carts_full_NN_jax[ np.argmax(V_nonlocal_full_NN_jax) @@ -338,24 +334,24 @@ def test_debug_and_jax_ecp_non_local_full_NN(Nv, alpha, beta, gamma): np.testing.assert_allclose( V_ecp_non_local_max_full_NN_jax, V_ecp_non_local_max_full_NN_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_non_local_r_up_carts_max_full_NN_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_non_local_r_up_carts_max_full_NN_debug))), "NaN detected in second argument" np.testing.assert_allclose( mesh_non_local_r_up_carts_max_full_NN_jax, mesh_non_local_r_up_carts_max_full_NN_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_non_local_r_dn_carts_max_full_NN_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_non_local_r_dn_carts_max_full_NN_debug))), "NaN detected in second argument" np.testing.assert_allclose( mesh_non_local_r_dn_carts_max_full_NN_jax, mesh_non_local_r_dn_carts_max_full_NN_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) # ecp non-local (NN, N=max) @@ -395,16 +391,14 @@ def test_debug_and_jax_ecp_non_local_full_NN(Nv, alpha, beta, gamma): np.testing.assert_allclose( sum_V_nonlocal_full_NN_debug, sum_V_nonlocal_NN_check_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) # jax, full-NN vs check-NN assert not np.any(np.isnan(np.asarray(sum_V_nonlocal_full_NN_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(sum_V_nonlocal_NN_check_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - sum_V_nonlocal_full_NN_jax, sum_V_nonlocal_NN_check_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(sum_V_nonlocal_full_NN_jax, sum_V_nonlocal_NN_check_jax, atol=atol, rtol=rtol) mesh_non_local_r_up_carts_max_NN_check_jax = mesh_non_local_ecp_part_r_up_carts_NN_check_jax[ np.argmax(V_nonlocal_NN_check_jax) @@ -427,24 +421,24 @@ def test_debug_and_jax_ecp_non_local_full_NN(Nv, alpha, beta, gamma): np.testing.assert_allclose( V_ecp_non_local_max_full_NN_debug, V_ecp_non_local_max_NN_check_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_non_local_r_up_carts_max_full_NN_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_non_local_r_up_carts_max_NN_check_debug))), "NaN detected in second argument" np.testing.assert_allclose( mesh_non_local_r_up_carts_max_full_NN_debug, mesh_non_local_r_up_carts_max_NN_check_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_non_local_r_dn_carts_max_full_NN_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_non_local_r_dn_carts_max_NN_check_debug))), "NaN detected in second argument" np.testing.assert_allclose( mesh_non_local_r_dn_carts_max_full_NN_debug, mesh_non_local_r_dn_carts_max_NN_check_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) # jax, full-NN vs check-NN @@ -453,24 +447,24 @@ def test_debug_and_jax_ecp_non_local_full_NN(Nv, alpha, beta, gamma): np.testing.assert_allclose( V_ecp_non_local_max_full_NN_jax, V_ecp_non_local_max_NN_check_jax, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_non_local_r_up_carts_max_full_NN_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_non_local_r_up_carts_max_NN_check_jax))), "NaN detected in second argument" np.testing.assert_allclose( mesh_non_local_r_up_carts_max_full_NN_jax, mesh_non_local_r_up_carts_max_NN_check_jax, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_non_local_r_dn_carts_max_full_NN_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_non_local_r_dn_carts_max_NN_check_jax))), "NaN detected in second argument" np.testing.assert_allclose( mesh_non_local_r_dn_carts_max_full_NN_jax, mesh_non_local_r_dn_carts_max_NN_check_jax, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) @@ -479,6 +473,7 @@ def test_debug_and_jax_ecp_non_local_full_NN(Nv, alpha, beta, gamma): @pytest.mark.parametrize("alpha, beta, gamma", angle_params) def test_debug_and_jax_ecp_non_local_partial_NN(Nv, alpha, beta, gamma): """Test the non-local ECP potential computation with partial neibohrs.""" + atol, rtol = get_tolerance("coulomb", "strict") ( structure_data, _, @@ -572,9 +567,7 @@ def test_debug_and_jax_ecp_non_local_partial_NN(Nv, alpha, beta, gamma): assert not np.any(np.isnan(np.asarray(sum_V_nonlocal_NN_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(sum_V_nonlocal_NN_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - sum_V_nonlocal_NN_debug, sum_V_nonlocal_NN_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(sum_V_nonlocal_NN_debug, sum_V_nonlocal_NN_jax, atol=atol, rtol=rtol) mesh_non_local_r_up_carts_max_NN_jax = mesh_non_local_ecp_part_r_up_carts_NN_jax[np.argmax(V_nonlocal_NN_jax)] mesh_non_local_r_up_carts_max_NN_debug = mesh_non_local_ecp_part_r_up_carts_NN_debug[np.argmax(V_nonlocal_NN_debug)] @@ -588,24 +581,24 @@ def test_debug_and_jax_ecp_non_local_partial_NN(Nv, alpha, beta, gamma): np.testing.assert_allclose( V_ecp_non_local_max_NN_jax, V_ecp_non_local_max_NN_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_non_local_r_up_carts_max_NN_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_non_local_r_up_carts_max_NN_debug))), "NaN detected in second argument" np.testing.assert_allclose( mesh_non_local_r_up_carts_max_NN_jax, mesh_non_local_r_up_carts_max_NN_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_non_local_r_dn_carts_max_NN_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_non_local_r_dn_carts_max_NN_debug))), "NaN detected in second argument" np.testing.assert_allclose( mesh_non_local_r_dn_carts_max_NN_jax, mesh_non_local_r_dn_carts_max_NN_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) @@ -638,6 +631,7 @@ def _build_full_jastrow_data(structure_data, geminal_mo_data, coulomb_potential_ @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5"]) def test_fast_update_ecp_non_local_partial_NN(trexio_file: str): """Fast-update nearest-neighbor ECP matches reference with all Jastrow terms enabled.""" + atol, rtol = get_tolerance("coulomb", "strict") ( structure_data, _aos_data, @@ -703,29 +697,22 @@ def test_fast_update_ecp_non_local_partial_NN(trexio_file: str): assert not np.any(np.isnan(np.asarray(np.asarray(sum_V_fast)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(sum_V_ref)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(sum_V_fast), np.asarray(sum_V_ref), atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(np.asarray(sum_V_fast), np.asarray(sum_V_ref), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(V_fast)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(V_ref)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(V_fast), np.asarray(V_ref), atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(np.asarray(V_fast), np.asarray(V_ref), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(mesh_up_fast)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(mesh_up_ref)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(mesh_up_fast), np.asarray(mesh_up_ref), atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(np.asarray(mesh_up_fast), np.asarray(mesh_up_ref), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(mesh_dn_fast)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(mesh_dn_ref)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(mesh_dn_fast), np.asarray(mesh_dn_ref), atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(np.asarray(mesh_dn_fast), np.asarray(mesh_dn_ref), atol=atol, rtol=rtol) @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5"]) def test_debug_and_jax_bare_el_ion_elements(trexio_file: str): """Test the bare couloumb potential computation.""" + atol, rtol = get_tolerance("coulomb", "strict") ( structure_data, _, @@ -774,19 +761,16 @@ def test_debug_and_jax_bare_el_ion_elements(trexio_file: str): assert not np.any(np.isnan(np.asarray(interactions_R_r_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(interactions_R_r_up_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - interactions_R_r_up_debug, interactions_R_r_up_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(interactions_R_r_up_debug, interactions_R_r_up_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(interactions_R_r_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(interactions_R_r_dn_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - interactions_R_r_dn_debug, interactions_R_r_dn_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(interactions_R_r_dn_debug, interactions_R_r_dn_jax, atol=atol, rtol=rtol) @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5"]) def test_debug_and_jax_discretized_bare_el_ion_elements(trexio_file: str): """Test the bare couloumb potential computation.""" + atol, rtol = get_tolerance("coulomb", "strict") ( structure_data, _, @@ -841,14 +825,10 @@ def test_debug_and_jax_discretized_bare_el_ion_elements(trexio_file: str): assert not np.any(np.isnan(np.asarray(interactions_R_r_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(interactions_R_r_up_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - interactions_R_r_up_debug, interactions_R_r_up_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(interactions_R_r_up_debug, interactions_R_r_up_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(interactions_R_r_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(interactions_R_r_dn_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - interactions_R_r_dn_debug, interactions_R_r_dn_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(interactions_R_r_dn_debug, interactions_R_r_dn_jax, atol=atol, rtol=rtol) @pytest.mark.parametrize("Nv", Nv_params) @@ -859,6 +839,7 @@ def test_compute_ecp_coulomb_potential_fast(Nv, alpha, beta, gamma): With a freshly computed LU inverse both functions must agree to full double precision because the only difference is which code path owns the LU solve. """ + atol, rtol = get_tolerance("coulomb", "strict") ( _structure_data, _aos_data, @@ -915,9 +896,7 @@ def test_compute_ecp_coulomb_potential_fast(Nv, alpha, beta, gamma): assert not np.any(np.isnan(np.asarray(V_ref))), "NaN detected in V_ref" assert not np.any(np.isnan(np.asarray(V_fast))), "NaN detected in V_fast" - np.testing.assert_allclose( - np.asarray(V_fast), np.asarray(V_ref), atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(np.asarray(V_fast), np.asarray(V_ref), atol=atol, rtol=rtol) @pytest.mark.parametrize("Nv", Nv_params) @@ -929,6 +908,7 @@ def test_compute_coulomb_potential_fast(Nv, alpha, beta, gamma): Psi(r')/Psi(r) ratios must be identical between the two paths because the same A_old_inv (exact LU inverse) is used. """ + atol, rtol = get_tolerance("coulomb", "strict") ( _structure_data, _aos_data, @@ -985,9 +965,7 @@ def test_compute_coulomb_potential_fast(Nv, alpha, beta, gamma): assert not np.any(np.isnan(np.asarray(V_ref))), "NaN detected in V_ref" assert not np.any(np.isnan(np.asarray(V_fast))), "NaN detected in V_fast" - np.testing.assert_allclose( - np.asarray(V_fast), np.asarray(V_ref), atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(np.asarray(V_fast), np.asarray(V_ref), atol=atol, rtol=rtol) if __name__ == "__main__": diff --git a/tests/test_hamiltonian.py b/tests/test_hamiltonian.py index de7b3eb9..cbf87b41 100644 --- a/tests/test_hamiltonian.py +++ b/tests/test_hamiltonian.py @@ -45,14 +45,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import ( # noqa: E402 - atol_auto_vs_analytic_deriv, - atol_consistency, - atol_debug_vs_production, - rtol_auto_vs_analytic_deriv, - rtol_consistency, - rtol_debug_vs_production, -) +from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 from jqmc.determinant import ( Geminal_data, # noqa: E402 compute_geminal_all_elements, # noqa: E402 @@ -222,6 +215,7 @@ def test_compute_local_energy_fast(trexio_file): n_up = geminal_data.num_electron_up n_dn = geminal_data.num_electron_dn + atol, rtol = get_tolerance("kinetic", "strict") for _ in range(10): r_up = jnp.array(first_nucleus + rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(first_nucleus + rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) @@ -237,8 +231,8 @@ def test_compute_local_energy_fast(trexio_file): np.testing.assert_allclose( e_fast, e_ref, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, err_msg=f"compute_local_energy_fast={e_fast:.10f} != compute_local_energy={e_ref:.10f}", ) @@ -247,10 +241,16 @@ def _compare_grad_leaves( grad_ref, grad_test, label, - atol=atol_auto_vs_analytic_deriv, - rtol=rtol_auto_vs_analytic_deriv, + atol=None, + rtol=None, ): """Flatten two pytrees and compare every leaf.""" + if atol is None or rtol is None: + _atol, _rtol = get_tolerance_min(["geminal", "jastrow"], "strict") + if atol is None: + atol = _atol + if rtol is None: + rtol = _rtol leaves_ref = jax.tree_util.tree_leaves(grad_ref) leaves_tst = jax.tree_util.tree_leaves(grad_test) assert len(leaves_ref) == len(leaves_tst), f"{label}: number of leaves differ ({len(leaves_ref)} vs {len(leaves_tst)})" @@ -297,26 +297,30 @@ def test_grad_compute_local_energy(trexio_file): r_dn = jnp.array(first_nucleus + rng.standard_normal((n_dn, 3)) * 0.5, dtype=jnp.float64) # Sanity: both forward values must agree. + atol, rtol = get_tolerance("kinetic", "strict") e_auto = float(_compute_local_energy_auto(hamiltonian_data, r_up, r_dn, RT)) e_custom = float(compute_local_energy(hamiltonian_data, r_up, r_dn, RT)) np.testing.assert_allclose( e_custom, e_auto, - atol=atol_consistency, - rtol=rtol_consistency, + atol=atol, + rtol=rtol, err_msg="forward e_L mismatch", ) # Gradient comparison (w.r.t. full Hamiltonian pytree, argnums=0). + # Gradients flow through geminal and jastrow zones (float32 in mixed mode), + # so tolerance is bounded by the weakest zone on the path. grad_auto = jax.grad(_compute_local_energy_auto, argnums=0)(hamiltonian_data, r_up, r_dn, RT) grad_custom = jax.grad(compute_local_energy, argnums=0)(hamiltonian_data, r_up, r_dn, RT) + atol_grad, rtol_grad = get_tolerance_min(["geminal", "jastrow"], "strict") _compare_grad_leaves( grad_auto, grad_custom, label=f"grad(compute_local_energy) vs _auto [{trexio_file}, seed={seed}]", - atol=atol_auto_vs_analytic_deriv, - rtol=rtol_auto_vs_analytic_deriv, + atol=atol_grad, + rtol=rtol_grad, ) diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index 453b37b4..63e0f74d 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -43,16 +43,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import ( # noqa: E402 - atol_auto_vs_analytic_deriv, - atol_auto_vs_numerical_deriv, - atol_consistency, - atol_debug_vs_production, - rtol_auto_vs_analytic_deriv, - rtol_auto_vs_numerical_deriv, - rtol_consistency, - rtol_debug_vs_production, -) +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.atomic_orbital import AOs_sphe_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -89,6 +80,7 @@ @pytest.mark.parametrize("j1b_type", ["exp", "pade"]) def test_Jastrow_onebody_part(j1b_type): """Test the one-body Jastrow factor, comparing the debug and JAX implementations.""" + atol, rtol = get_tolerance("jastrow", "strict") num_r_up_cart_samples = 8 num_r_dn_cart_samples = 4 num_R_cart_samples = 6 @@ -128,14 +120,16 @@ def test_Jastrow_onebody_part(j1b_type): assert not np.any(np.isnan(np.asarray(J1_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(J1_jax))), "NaN detected in second argument" - np.testing.assert_allclose(J1_debug, J1_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(J1_debug, J1_jax, atol=atol, rtol=rtol) jax.clear_caches() +@pytest.mark.numerical_diff @pytest.mark.parametrize("j1b_type", ["exp", "pade"]) def test_numerical_and_auto_grads_Jastrow_onebody_part(j1b_type): """Test numerical and JAX grads of the one-body Jastrow factor.""" + atol, rtol = get_tolerance("kinetic", "loose") num_r_up_cart_samples = 6 num_r_dn_cart_samples = 3 num_R_cart_samples = 5 @@ -172,29 +166,25 @@ def test_numerical_and_auto_grads_Jastrow_onebody_part(j1b_type): assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_up_num), np.asarray(grad_up_auto), atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(np.asarray(grad_up_num), np.asarray(grad_up_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_dn_num), np.asarray(grad_dn_auto), atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(np.asarray(grad_dn_num), np.asarray(grad_dn_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_up_num), np.asarray(lap_up_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol, + atol=atol, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_dn_num), np.asarray(lap_dn_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol, + atol=atol, ) jax.clear_caches() @@ -203,6 +193,7 @@ def test_numerical_and_auto_grads_Jastrow_onebody_part(j1b_type): @pytest.mark.parametrize("j1b_type", ["exp", "pade"]) def test_analytical_and_auto_grads_Jastrow_onebody_part(j1b_type): """Analytic vs auto-diff gradients/laplacian for one-body Jastrow.""" + atol, rtol = get_tolerance("kinetic", "strict") num_r_up_cart_samples = 5 num_r_dn_cart_samples = 4 num_R_cart_samples = 4 @@ -239,24 +230,16 @@ def test_analytical_and_auto_grads_Jastrow_onebody_part(j1b_type): assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_up_an), np.asarray(grad_up_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(grad_up_an), np.asarray(grad_up_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_dn_an), np.asarray(grad_dn_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(grad_dn_an), np.asarray(grad_dn_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(lap_up_an), np.asarray(lap_up_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(lap_up_an), np.asarray(lap_up_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(lap_dn_an), np.asarray(lap_dn_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(lap_dn_an), np.asarray(lap_dn_auto), atol=atol, rtol=rtol) jax.clear_caches() @@ -264,6 +247,7 @@ def test_analytical_and_auto_grads_Jastrow_onebody_part(j1b_type): @pytest.mark.parametrize("j2b_type", ["pade", "exp"]) def test_Jastrow_twobody_part(j2b_type): """Test the two-body Jastrow factor, comparing the debug and JAX implementations.""" + atol, rtol = get_tolerance("jastrow", "strict") num_r_up_cart_samples = 5 num_r_dn_cart_samples = 2 @@ -285,15 +269,18 @@ def test_Jastrow_twobody_part(j2b_type): assert not np.any(np.isnan(np.asarray(J2_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(J2_jax))), "NaN detected in second argument" - np.testing.assert_allclose(J2_debug, J2_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(J2_debug, J2_jax, atol=atol, rtol=rtol) jax.clear_caches() @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff @pytest.mark.parametrize("j2b_type", ["pade", "exp"]) def test_numerical_and_auto_grads_Jastrow_twobody_part(j2b_type): """Test numerical and JAX grads of the two-body Jastrow factor, comparing the debug and JAX implementations.""" + atol_s, rtol_s = get_tolerance("jastrow", "strict") + atol_l, rtol_l = get_tolerance("kinetic", "loose") num_r_up_cart_samples = 5 num_r_dn_cart_samples = 2 @@ -315,7 +302,7 @@ def test_numerical_and_auto_grads_Jastrow_twobody_part(j2b_type): assert not np.any(np.isnan(np.asarray(J2_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(J2_jax))), "NaN detected in second argument" - np.testing.assert_allclose(J2_debug, J2_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(J2_debug, J2_jax, atol=atol_s, rtol=rtol_s) ( grad_J2_up_debug, @@ -344,29 +331,25 @@ def test_numerical_and_auto_grads_Jastrow_twobody_part(j2b_type): assert not np.any(np.isnan(np.asarray(grad_J2_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_J2_up_auto))), "NaN detected in second argument" - np.testing.assert_allclose( - grad_J2_up_debug, grad_J2_up_auto, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(grad_J2_up_debug, grad_J2_up_auto, atol=atol_l, rtol=rtol_l) assert not np.any(np.isnan(np.asarray(grad_J2_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_J2_dn_auto))), "NaN detected in second argument" - np.testing.assert_allclose( - grad_J2_dn_debug, grad_J2_dn_auto, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(grad_J2_dn_debug, grad_J2_dn_auto, atol=atol_l, rtol=rtol_l) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J2_up_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J2_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_J2_up_debug), np.asarray(lap_J2_up_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol_l, + atol=atol_l, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J2_dn_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J2_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_J2_dn_debug), np.asarray(lap_J2_dn_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol_l, + atol=atol_l, ) jax.clear_caches() @@ -376,6 +359,7 @@ def test_numerical_and_auto_grads_Jastrow_twobody_part(j2b_type): @pytest.mark.parametrize("j2b_type", ["pade", "exp"]) def test_analytic_and_auto_grads_Jastrow_twobody_part(j2b_type): """Analytic vs auto-diff gradients/laplacian for two-body Jastrow.""" + atol, rtol = get_tolerance("kinetic", "strict") num_r_up_cart_samples = 5 num_r_dn_cart_samples = 2 @@ -405,32 +389,32 @@ def test_analytic_and_auto_grads_Jastrow_twobody_part(j2b_type): np.testing.assert_allclose( np.asarray(grad_J2_up_analytic), np.asarray(grad_J2_up_auto), - atol=atol_auto_vs_analytic_deriv, - rtol=rtol_auto_vs_analytic_deriv, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.asarray(grad_J2_dn_analytic)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_J2_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(grad_J2_dn_analytic), np.asarray(grad_J2_dn_auto), - atol=atol_auto_vs_analytic_deriv, - rtol=rtol_auto_vs_analytic_deriv, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J2_up_analytic)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J2_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_J2_up_analytic), np.asarray(lap_J2_up_auto), - atol=atol_auto_vs_analytic_deriv, - rtol=rtol_auto_vs_analytic_deriv, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J2_dn_analytic)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J2_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_J2_dn_analytic), np.asarray(lap_J2_dn_auto), - atol=atol_auto_vs_analytic_deriv, - rtol=rtol_auto_vs_analytic_deriv, + atol=atol, + rtol=rtol, ) jax.clear_caches() @@ -438,6 +422,7 @@ def test_analytic_and_auto_grads_Jastrow_twobody_part(j2b_type): def test_Jastrow_threebody_part_with_AOs_data(): """Test the three-body Jastrow factor, comparing the debug and JAX implementations, using AOs data.""" + atol, rtol = get_tolerance("jastrow", "strict") num_r_up_cart_samples = 4 num_r_dn_cart_samples = 2 num_R_cart_samples = 6 @@ -504,13 +489,14 @@ def test_Jastrow_threebody_part_with_AOs_data(): assert not np.any(np.isnan(np.asarray(J3_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(J3_jax))), "NaN detected in second argument" - np.testing.assert_allclose(J3_debug, J3_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(J3_debug, J3_jax, atol=atol, rtol=rtol) jax.clear_caches() def test_Jastrow_threebody_part_with_MOs_data(): """Test the three-body Jastrow factor, comparing the debug and JAX implementations, using MOs data.""" + atol, rtol = get_tolerance("jastrow", "strict") num_el = 10 num_mo = 5 num_ao = 3 @@ -581,7 +567,7 @@ def test_Jastrow_threebody_part_with_MOs_data(): assert not np.any(np.isnan(np.asarray(J3_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(J3_jax))), "NaN detected in second argument" - np.testing.assert_allclose(J3_debug, J3_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(J3_debug, J3_jax, atol=atol, rtol=rtol) jax.clear_caches() @@ -589,6 +575,7 @@ def test_Jastrow_threebody_part_with_MOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_sphe_to_cart_AOs_data(): """Round-trip AOs l<=6: spherical→Cartesian keeps J3 values/grads.""" + atol, rtol = get_tolerance("jastrow", "strict") rng = np.random.default_rng(321) nucleus_index: list[int] = [] @@ -646,11 +633,11 @@ def test_Jastrow_threebody_part_sphe_to_cart_AOs_data(): assert not np.any(np.isnan(np.asarray(np.asarray(J_sph)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(J_cart)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(J_sph), np.asarray(J_cart), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(J_sph), np.asarray(J_cart), atol=atol, rtol=rtol) for sph, cart in zip(grads_sph, grads_cart, strict=True): assert not np.any(np.isnan(np.asarray(np.asarray(sph)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(cart)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(sph), np.asarray(cart), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(sph), np.asarray(cart), atol=atol, rtol=rtol) jax.clear_caches() @@ -658,6 +645,7 @@ def test_Jastrow_threebody_part_sphe_to_cart_AOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_cart_to_sphe_AOs_data(): """Round-trip AOs l<=6: Cartesian→spherical keeps J3 values/grads.""" + atol, rtol = get_tolerance("jastrow", "strict") rng = np.random.default_rng(654) nucleus_index: list[int] = [] @@ -715,11 +703,11 @@ def test_Jastrow_threebody_part_cart_to_sphe_AOs_data(): assert not np.any(np.isnan(np.asarray(np.asarray(J_cart)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(J_sph)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(J_cart), np.asarray(J_sph), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(J_cart), np.asarray(J_sph), atol=atol, rtol=rtol) for cart, sph in zip(grads_cart, grads_sph, strict=True): assert not np.any(np.isnan(np.asarray(np.asarray(cart)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(sph)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(cart), np.asarray(sph), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(cart), np.asarray(sph), atol=atol, rtol=rtol) jax.clear_caches() @@ -727,6 +715,7 @@ def test_Jastrow_threebody_part_cart_to_sphe_AOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_sphe_to_cart_MOs_data(): """Round-trip MOs built on l<=6 AOs: spherical→Cartesian keeps J3 values/grads.""" + atol, rtol = get_tolerance("jastrow", "strict") rng = np.random.default_rng(777) nucleus_index: list[int] = [] @@ -791,11 +780,11 @@ def test_Jastrow_threebody_part_sphe_to_cart_MOs_data(): assert not np.any(np.isnan(np.asarray(np.asarray(J_sph)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(J_cart)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(J_sph), np.asarray(J_cart), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(J_sph), np.asarray(J_cart), atol=atol, rtol=rtol) for sph, cart in zip(grads_sph, grads_cart, strict=True): assert not np.any(np.isnan(np.asarray(np.asarray(sph)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(cart)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(sph), np.asarray(cart), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(sph), np.asarray(cart), atol=atol, rtol=rtol) jax.clear_caches() @@ -803,6 +792,7 @@ def test_Jastrow_threebody_part_sphe_to_cart_MOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_cart_to_sphe_MOs_data(): """Round-trip MOs l<=6: Cartesian→spherical keeps J3 values/grads.""" + atol, rtol = get_tolerance("jastrow", "strict") rng = np.random.default_rng(888) nucleus_index: list[int] = [] @@ -867,18 +857,21 @@ def test_Jastrow_threebody_part_cart_to_sphe_MOs_data(): assert not np.any(np.isnan(np.asarray(np.asarray(J_cart)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(J_sph)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(J_cart), np.asarray(J_sph), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(J_cart), np.asarray(J_sph), atol=atol, rtol=rtol) for cart, sph in zip(grads_cart, grads_sph, strict=True): assert not np.any(np.isnan(np.asarray(np.asarray(cart)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(sph)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(cart), np.asarray(sph), atol=atol_consistency, rtol=rtol_consistency) + np.testing.assert_allclose(np.asarray(cart), np.asarray(sph), atol=atol, rtol=rtol) jax.clear_caches() @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): """Test numerical and JAX grads of the three-body Jastrow factor, comparing the debug and JAX implementations, using AOs data.""" + atol_s, rtol_s = get_tolerance("jastrow", "strict") + atol_l, rtol_l = get_tolerance("kinetic", "loose") num_r_up_cart_samples = 4 num_r_dn_cart_samples = 2 num_R_cart_samples = 6 @@ -941,7 +934,7 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): assert not np.any(np.isnan(np.asarray(J3_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(J3_jax))), "NaN detected in second argument" - np.testing.assert_allclose(J3_debug, J3_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(J3_debug, J3_jax, atol=atol_s, rtol=rtol_s) ( grad_jastrow_J3_up_debug, @@ -964,37 +957,36 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_up_auto))), "NaN detected in second argument" - np.testing.assert_allclose( - grad_jastrow_J3_up_debug, grad_jastrow_J3_up_auto, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(grad_jastrow_J3_up_debug, grad_jastrow_J3_up_auto, atol=atol_l, rtol=rtol_l) assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_dn_auto))), "NaN detected in second argument" - np.testing.assert_allclose( - grad_jastrow_J3_dn_debug, grad_jastrow_J3_dn_auto, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(grad_jastrow_J3_dn_debug, grad_jastrow_J3_dn_auto, atol=atol_l, rtol=rtol_l) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_up_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_J3_up_debug), np.asarray(lap_J3_up_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol_l, + atol=atol_l, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_dn_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_J3_dn_debug), np.asarray(lap_J3_dn_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol_l, + atol=atol_l, ) jax.clear_caches() @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): """Test numerical and JAX grads of the three-body Jastrow factor, comparing the debug and JAX implementations, using MOs data.""" + atol_s, rtol_s = get_tolerance("jastrow", "strict") + atol_l, rtol_l = get_tolerance("kinetic", "loose") num_el = 10 num_mo = 5 num_ao = 3 @@ -1061,7 +1053,7 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): assert not np.any(np.isnan(np.asarray(J3_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(J3_jax))), "NaN detected in second argument" - np.testing.assert_allclose(J3_debug, J3_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(J3_debug, J3_jax, atol=atol_s, rtol=rtol_s) ( grad_jastrow_J3_up_debug, @@ -1084,30 +1076,26 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_up_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - grad_jastrow_J3_up_debug, grad_jastrow_J3_up_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(grad_jastrow_J3_up_debug, grad_jastrow_J3_up_jax, atol=atol_l, rtol=rtol_l) assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_dn_jax))), "NaN detected in second argument" - np.testing.assert_allclose( - grad_jastrow_J3_dn_debug, grad_jastrow_J3_dn_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(grad_jastrow_J3_dn_debug, grad_jastrow_J3_dn_jax, atol=atol_l, rtol=rtol_l) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_up_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_up_jax)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_J3_up_debug), np.asarray(lap_J3_up_jax), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol_l, + atol=atol_l, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_dn_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_dn_jax)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_J3_dn_debug), np.asarray(lap_J3_dn_jax), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol_l, + atol=atol_l, ) jax.clear_caches() @@ -1116,6 +1104,7 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): @pytest.mark.activate_if_skip_heavy def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): """Analytic vs auto-diff gradients/laplacian for three-body Jastrow (AOs).""" + atol, rtol = get_tolerance("kinetic", "strict") num_r_up_cart_samples = 4 num_r_dn_cart_samples = 2 num_R_cart_samples = 5 @@ -1170,24 +1159,16 @@ def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_up_an), np.asarray(grad_up_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(grad_up_an), np.asarray(grad_up_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_dn_an), np.asarray(grad_dn_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(grad_dn_an), np.asarray(grad_dn_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(lap_up_an), np.asarray(lap_up_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(lap_up_an), np.asarray(lap_up_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(lap_dn_an), np.asarray(lap_dn_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(lap_dn_an), np.asarray(lap_dn_auto), atol=atol, rtol=rtol) jax.clear_caches() @@ -1195,6 +1176,7 @@ def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): @pytest.mark.activate_if_skip_heavy def test_analytic_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): """Analytic vs auto-diff gradients/laplacian for three-body Jastrow (MOs).""" + atol, rtol = get_tolerance("kinetic", "strict") num_el = 8 num_mo = 4 num_ao = 3 @@ -1252,24 +1234,16 @@ def test_analytic_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_up_an), np.asarray(grad_up_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(grad_up_an), np.asarray(grad_up_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_dn_an), np.asarray(grad_dn_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(grad_dn_an), np.asarray(grad_dn_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(lap_up_an), np.asarray(lap_up_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(lap_up_an), np.asarray(lap_up_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(lap_dn_an), np.asarray(lap_dn_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(lap_dn_an), np.asarray(lap_dn_auto), atol=atol, rtol=rtol) jax.clear_caches() @@ -1358,9 +1332,11 @@ def _build_jastrow_data_for_part_tests(j1b_type: str = "exp", j2b_type: str = "p @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff @pytest.mark.parametrize("j1b_type,j2b_type,include_nn", _JASTROW_COMBOS) def test_numerical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): """Numerical vs auto-diff gradients/laplacian for J1+J2+J3(+NN).""" + atol, rtol = get_tolerance("kinetic", "loose") jastrow_data, r_up_carts, r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) grad_up_num, grad_dn_num, lap_up_num, lap_dn_num = _compute_grads_and_laplacian_Jastrow_part_debug( @@ -1377,30 +1353,26 @@ def test_numerical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_up_num), np.asarray(grad_up_auto), atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(np.asarray(grad_up_num), np.asarray(grad_up_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_dn_num), np.asarray(grad_dn_auto), atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(np.asarray(grad_dn_num), np.asarray(grad_dn_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_up_num), np.asarray(lap_up_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol, + atol=atol, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_dn_num), np.asarray(lap_dn_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol, + atol=atol, ) jax.clear_caches() @@ -1410,6 +1382,7 @@ def test_numerical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): @pytest.mark.parametrize("j1b_type,j2b_type,include_nn", _JASTROW_COMBOS) def test_analytical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): """Analytic vs auto-diff gradients/laplacian for J1+J2+J3(+NN).""" + atol, rtol = get_tolerance("kinetic", "strict") jastrow_data, r_up_carts, r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) grad_up_an, grad_dn_an, lap_up_an, lap_dn_an = compute_grads_and_laplacian_Jastrow_part( @@ -1426,24 +1399,16 @@ def test_analytical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_up_an), np.asarray(grad_up_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(grad_up_an), np.asarray(grad_up_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_dn_an), np.asarray(grad_dn_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(grad_dn_an), np.asarray(grad_dn_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(lap_up_an), np.asarray(lap_up_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(lap_up_an), np.asarray(lap_up_auto), atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_an)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(lap_dn_an), np.asarray(lap_dn_auto), atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(np.asarray(lap_dn_an), np.asarray(lap_dn_auto), atol=atol, rtol=rtol) jax.clear_caches() @@ -1453,6 +1418,7 @@ def test_analytical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): @pytest.mark.parametrize("pattern", ["all_moved", "none_moved", "mixed"]) def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern: str): """Compare ratio Jastrow part: debug vs rank-1 update implementation.""" + atol, rtol = get_tolerance("jastrow", "strict") np.random.seed(0) jastrow_data, old_r_up_carts, old_r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) @@ -1491,16 +1457,12 @@ def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern assert not np.any(np.isnan(np.asarray(np.asarray(ratio_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(ratio_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(ratio_debug), np.asarray(ratio_auto), atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(np.asarray(ratio_debug), np.asarray(ratio_auto), atol=atol, rtol=rtol) if pattern == "none_moved": assert not np.any(np.isnan(np.asarray(np.asarray(ratio_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.ones_like(np.asarray(ratio_debug))))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(ratio_debug), np.ones_like(np.asarray(ratio_debug)), atol=atol_consistency, rtol=rtol_consistency - ) + np.testing.assert_allclose(np.asarray(ratio_debug), np.ones_like(np.asarray(ratio_debug)), atol=atol, rtol=rtol) jax.clear_caches() diff --git a/tests/test_jqmc_gfmc_bra.py b/tests/test_jqmc_gfmc_bra.py index ca199f83..121c511a 100755 --- a/tests/test_jqmc_gfmc_bra.py +++ b/tests/test_jqmc_gfmc_bra.py @@ -45,7 +45,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import atol_debug_vs_production, rtol_debug_vs_production # noqa: E402 +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -181,27 +181,29 @@ def test_jqmc_gfmc_n(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast ) gfmc_jax.run(num_mcmc_steps=num_mcmc_steps) + atol, rtol = get_tolerance("gfmc", "strict") + if mpi_rank == 0: # w_L w_L_debug = gfmc_debug.w_L w_L_jax = gfmc_jax.w_L assert not np.any(np.isnan(np.asarray(w_L_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(w_L_jax))), "NaN detected in second argument" - np.testing.assert_allclose(w_L_debug, w_L_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(w_L_debug, w_L_jax, atol=atol, rtol=rtol) # e_L e_L_debug = gfmc_debug.e_L e_L_jax = gfmc_jax.e_L assert not np.any(np.isnan(np.asarray(e_L_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(e_L_jax))), "NaN detected in second argument" - np.testing.assert_allclose(e_L_debug, e_L_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(e_L_debug, e_L_jax, atol=atol, rtol=rtol) # e_L2 e_L2_debug = gfmc_debug.e_L2 e_L2_jax = gfmc_jax.e_L2 assert not np.any(np.isnan(np.asarray(e_L2_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(e_L2_jax))), "NaN detected in second argument" - np.testing.assert_allclose(e_L2_debug, e_L2_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(e_L2_debug, e_L2_jax, atol=atol, rtol=rtol) # E E_debug, E_err_debug, Var_debug, Var_err_debug = gfmc_debug.get_E( @@ -214,16 +216,16 @@ def test_jqmc_gfmc_n(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast ) assert not np.any(np.isnan(np.asarray(E_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(E_jax))), "NaN detected in second argument" - np.testing.assert_allclose(E_debug, E_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(E_debug, E_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(E_err_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(E_err_jax))), "NaN detected in second argument" - np.testing.assert_allclose(E_err_debug, E_err_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(E_err_debug, E_err_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(Var_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(Var_jax))), "NaN detected in second argument" - np.testing.assert_allclose(Var_debug, Var_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(Var_debug, Var_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(Var_err_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(Var_err_jax))), "NaN detected in second argument" - np.testing.assert_allclose(Var_err_debug, Var_err_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(Var_err_debug, Var_err_jax, atol=atol, rtol=rtol) # aF force_mean_debug, force_std_debug = gfmc_debug.get_aF( @@ -236,10 +238,10 @@ def test_jqmc_gfmc_n(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast ) assert not np.any(np.isnan(np.asarray(force_mean_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(force_mean_jax))), "NaN detected in second argument" - np.testing.assert_allclose(force_mean_debug, force_mean_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(force_mean_debug, force_mean_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(force_std_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(force_std_jax))), "NaN detected in second argument" - np.testing.assert_allclose(force_std_debug, force_std_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(force_std_debug, force_std_jax, atol=atol, rtol=rtol) jax.clear_caches() diff --git a/tests/test_jqmc_gfmc_tau.py b/tests/test_jqmc_gfmc_tau.py index fe554905..eab8a6cb 100755 --- a/tests/test_jqmc_gfmc_tau.py +++ b/tests/test_jqmc_gfmc_tau.py @@ -45,7 +45,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import atol_debug_vs_production, rtol_debug_vs_production # noqa: E402 +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -178,27 +178,29 @@ def test_jqmc_gfmc_t(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast ) gfmc_jax.run(num_mcmc_steps=num_mcmc_steps) + atol, rtol = get_tolerance("gfmc", "strict") + if mpi_rank == 0: # w_L w_L_debug = gfmc_debug.w_L w_L_jax = gfmc_jax.w_L assert not np.any(np.isnan(np.asarray(w_L_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(w_L_jax))), "NaN detected in second argument" - np.testing.assert_allclose(w_L_debug, w_L_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(w_L_debug, w_L_jax, atol=atol, rtol=rtol) # e_L e_L_debug = gfmc_debug.e_L e_L_jax = gfmc_jax.e_L assert not np.any(np.isnan(np.asarray(e_L_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(e_L_jax))), "NaN detected in second argument" - np.testing.assert_allclose(e_L_debug, e_L_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(e_L_debug, e_L_jax, atol=atol, rtol=rtol) # e_L2 e_L2_debug = gfmc_debug.e_L2 e_L2_jax = gfmc_jax.e_L2 assert not np.any(np.isnan(np.asarray(e_L2_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(e_L2_jax))), "NaN detected in second argument" - np.testing.assert_allclose(e_L2_debug, e_L2_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(e_L2_debug, e_L2_jax, atol=atol, rtol=rtol) # average_projection_counter # Both GFMC_t and _GFMC_t_debug now store local averages per rank. @@ -206,7 +208,7 @@ def test_jqmc_gfmc_t(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast apc_jax = gfmc_jax.average_projection_counter assert not np.any(np.isnan(np.asarray(apc_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(apc_jax))), "NaN detected in second argument" - np.testing.assert_allclose(apc_debug, apc_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(apc_debug, apc_jax, atol=atol, rtol=rtol) # E E_debug, E_err_debug, Var_debug, Var_err_debug = gfmc_debug.get_E( @@ -219,16 +221,16 @@ def test_jqmc_gfmc_t(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast ) assert not np.any(np.isnan(np.asarray(E_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(E_jax))), "NaN detected in second argument" - np.testing.assert_allclose(E_debug, E_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(E_debug, E_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(E_err_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(E_err_jax))), "NaN detected in second argument" - np.testing.assert_allclose(E_err_debug, E_err_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(E_err_debug, E_err_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(Var_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(Var_jax))), "NaN detected in second argument" - np.testing.assert_allclose(Var_debug, Var_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(Var_debug, Var_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(Var_err_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(Var_err_jax))), "NaN detected in second argument" - np.testing.assert_allclose(Var_err_debug, Var_err_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(Var_err_debug, Var_err_jax, atol=atol, rtol=rtol) # aF force_mean_debug, force_std_debug = gfmc_debug.get_aF( @@ -241,10 +243,10 @@ def test_jqmc_gfmc_t(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast ) assert not np.any(np.isnan(np.asarray(force_mean_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(force_mean_jax))), "NaN detected in second argument" - np.testing.assert_allclose(force_mean_debug, force_mean_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(force_mean_debug, force_mean_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(force_std_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(force_std_jax))), "NaN detected in second argument" - np.testing.assert_allclose(force_std_debug, force_std_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(force_std_debug, force_std_jax, atol=atol, rtol=rtol) jax.clear_caches() diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index 7954f054..d1e049be 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -47,7 +47,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import atol_debug_vs_production, rtol_debug_vs_production # noqa: E402 +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.determinant import Geminal_data # noqa: E402 from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 @@ -80,6 +80,7 @@ @pytest.mark.parametrize("trexio_file,with_1b_jastrow,with_2b_jastrow,with_3b_jastrow,with_nn_jastrow", param_grid) def test_jqmc_mcmc(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jastrow, with_nn_jastrow): """Test comparison with MCMC debug and MCMC production implementations.""" + atol, rtol = get_tolerance("mcmc", "strict") ( structure_data, _, @@ -173,21 +174,21 @@ def test_jqmc_mcmc(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jastro w_L_jax = mcmc_jax.w_L assert not np.any(np.isnan(np.asarray(w_L_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(w_L_jax))), "NaN detected in second argument" - np.testing.assert_allclose(w_L_debug, w_L_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(w_L_debug, w_L_jax, atol=atol, rtol=rtol) # e_L e_L_debug = mcmc_debug.e_L e_L_jax = mcmc_jax.e_L assert not np.any(np.isnan(np.asarray(e_L_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(e_L_jax))), "NaN detected in second argument" - np.testing.assert_allclose(e_L_debug, e_L_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(e_L_debug, e_L_jax, atol=atol, rtol=rtol) # e_L2 e_L2_debug = mcmc_debug.e_L2 e_L2_jax = mcmc_jax.e_L2 assert not np.any(np.isnan(np.asarray(e_L2_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(e_L2_jax))), "NaN detected in second argument" - np.testing.assert_allclose(e_L2_debug, e_L2_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(e_L2_debug, e_L2_jax, atol=atol, rtol=rtol) # E E_debug, E_err_debug, Var_debug, Var_err_debug = mcmc_debug.get_E( @@ -208,16 +209,16 @@ def test_jqmc_mcmc(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jastro assert not np.any(np.isnan(Var_err_jax)), f"Var_err_jax contains NaN: {Var_err_jax}" assert not np.any(np.isnan(np.asarray(E_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(E_jax))), "NaN detected in second argument" - np.testing.assert_allclose(E_debug, E_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(E_debug, E_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(E_err_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(E_err_jax))), "NaN detected in second argument" - np.testing.assert_allclose(E_err_debug, E_err_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(E_err_debug, E_err_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(Var_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(Var_jax))), "NaN detected in second argument" - np.testing.assert_allclose(Var_debug, Var_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(Var_debug, Var_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(Var_err_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(Var_err_jax))), "NaN detected in second argument" - np.testing.assert_allclose(Var_err_debug, Var_err_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(Var_err_debug, Var_err_jax, atol=atol, rtol=rtol) # aF force_mean_debug, force_std_debug = mcmc_debug.get_aF( @@ -234,10 +235,10 @@ def test_jqmc_mcmc(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jastro assert not np.any(np.isnan(force_std_jax)), f"force_std_jax contains NaN: {force_std_jax}" assert not np.any(np.isnan(np.asarray(force_mean_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(force_mean_jax))), "NaN detected in second argument" - np.testing.assert_allclose(force_mean_debug, force_mean_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(force_mean_debug, force_mean_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(force_std_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(force_std_jax))), "NaN detected in second argument" - np.testing.assert_allclose(force_std_debug, force_std_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(force_std_debug, force_std_jax, atol=atol, rtol=rtol) jax.clear_caches() @@ -1028,9 +1029,10 @@ def fake_get_gF( ln_psi_mo = float(evaluate_ln_wavefunction(final_wf, r_up, r_dn)) ln_psi_ao = float(evaluate_ln_wavefunction(wf_ao, r_up, r_dn)) + atol, rtol = get_tolerance("mcmc", "strict") assert not np.any(np.isnan(np.asarray(ln_psi_mo))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(ln_psi_ao))), "NaN detected in second argument" - np.testing.assert_allclose(ln_psi_mo, ln_psi_ao, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(ln_psi_mo, ln_psi_ao, atol=atol, rtol=rtol) jax.clear_caches() @@ -1209,12 +1211,13 @@ def fake_get_gF( lam_after = np.asarray(mcmc.hamiltonian_data.wavefunction_data.geminal_data.lambda_matrix) # ── Assertions ─────────────────────────────────────────────────────────── + atol, rtol = get_tolerance("mcmc", "strict") if j3_type == "sym": np.testing.assert_allclose( j3_after[:, :-1], j3_after[:, :-1].T, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, err_msg="j3 sub-block symmetry broken after VMC update", ) else: @@ -1225,8 +1228,8 @@ def fake_get_gF( np.testing.assert_allclose( lam_after, lam_after.T, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, err_msg="square lambda symmetry broken after VMC update", ) elif lambda_type == "rect_paired_sym": @@ -1234,8 +1237,8 @@ def fake_get_gF( np.testing.assert_allclose( lam_after[:, :n_paired], lam_after[:, :n_paired].T, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, err_msg="rectangular lambda paired sub-block symmetry broken after VMC update", ) else: @@ -1595,6 +1598,8 @@ def test_get_aH_and_solve_lm_debug_vs_production(): # Get variational blocks blocks = hamiltonian_data.wavefunction_data.get_variational_blocks() + atol, rtol = get_tolerance("mcmc", "strict") + # --- Test 1: get_aH in LM mode (return_matrices=True) --- H_0_d, f_d, S_d, K_d, B_d = mcmc_debug.get_aH( blocks=blocks, @@ -1607,11 +1612,11 @@ def test_get_aH_and_solve_lm_debug_vs_production(): return_matrices=True, ) - np.testing.assert_allclose(H_0_d, H_0_p, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) - np.testing.assert_allclose(f_d, f_p, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) - np.testing.assert_allclose(S_d, S_p, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) - np.testing.assert_allclose(K_d, K_p, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) - np.testing.assert_allclose(B_d, B_p, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(H_0_d, H_0_p, atol=atol, rtol=rtol) + np.testing.assert_allclose(f_d, f_p, atol=atol, rtol=rtol) + np.testing.assert_allclose(S_d, S_p, atol=atol, rtol=rtol) + np.testing.assert_allclose(K_d, K_p, atol=atol, rtol=rtol) + np.testing.assert_allclose(B_d, B_p, atol=atol, rtol=rtol) # --- Test 2: get_aH in aSR mode (return_matrices=False) --- # Use a simple direction vector g for the aSR scalar projection test @@ -1631,19 +1636,19 @@ def test_get_aH_and_solve_lm_debug_vs_production(): return_matrices=False, ) - np.testing.assert_allclose(H_0_d2, H_0_p2, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) - np.testing.assert_allclose(H_1_d, H_1_p, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) - np.testing.assert_allclose(H_2_d, H_2_p, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) - np.testing.assert_allclose(S_2_d, S_2_p, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(H_0_d2, H_0_p2, atol=atol, rtol=rtol) + np.testing.assert_allclose(H_1_d, H_1_p, atol=atol, rtol=rtol) + np.testing.assert_allclose(H_2_d, H_2_p, atol=atol, rtol=rtol) + np.testing.assert_allclose(S_2_d, S_2_p, atol=atol, rtol=rtol) # --- Test 3: aSR scalars should be consistent with LM matrices --- # H_1 = -1/2 g^T f, S_2 = g^T S g, H_2 = g^T (K+B) g H_1_from_mat = -0.5 * np.dot(g, f_d) S_2_from_mat = g @ S_d @ g H_2_from_mat = g @ (K_d + B_d) @ g - np.testing.assert_allclose(H_1_d, H_1_from_mat, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) - np.testing.assert_allclose(S_2_d, S_2_from_mat, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) - np.testing.assert_allclose(H_2_d, H_2_from_mat, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(H_1_d, H_1_from_mat, atol=atol, rtol=rtol) + np.testing.assert_allclose(S_2_d, S_2_from_mat, atol=atol, rtol=rtol) + np.testing.assert_allclose(H_2_d, H_2_from_mat, atol=atol, rtol=rtol) # --- Test 4: solve_linear_method with identical inputs --- # Use the production matrices for both to verify the two implementations @@ -1651,8 +1656,8 @@ def test_get_aH_and_solve_lm_debug_vs_production(): epsilon_lm = 1e-6 c_debug, E_debug = _MCMC_debug.solve_linear_method(H_0_p, f_p, S_p, K_p, B_p, epsilon_lm) c_prod, E_prod = MCMC.solve_linear_method(H_0_p, f_p, S_p, K_p, B_p, epsilon_lm) - np.testing.assert_allclose(c_debug, c_prod, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) - np.testing.assert_allclose(E_debug, E_prod, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(c_debug, c_prod, atol=atol, rtol=rtol) + np.testing.assert_allclose(E_debug, E_prod, atol=atol, rtol=rtol) jax.clear_caches() diff --git a/tests/test_jqmc_tool.py b/tests/test_jqmc_tool.py index 4b445cac..2f8cfcda 100644 --- a/tests/test_jqmc_tool.py +++ b/tests/test_jqmc_tool.py @@ -61,10 +61,7 @@ vmc_analyze_output, vmc_generate_input, ) -from jqmc._setting import ( # noqa: E402 - atol_consistency, - rtol_consistency, -) +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 trexio_files = [ @@ -607,8 +604,9 @@ def test_mcmc_random_energy_jackknife(self, tmp_path): m = re.search(r"E\s*=\s*([+-]?[\d.eE+-]+)\s*\+-\s*([\d.eE+-]+)", result.output) assert m is not None E_cli, std_cli = float(m.group(1)), float(m.group(2)) - np.testing.assert_allclose(E_cli, E_ref, atol=atol_consistency, rtol=rtol_consistency) - np.testing.assert_allclose(std_cli, std_ref, atol=atol_consistency, rtol=rtol_consistency) + atol, rtol = get_tolerance("io", "strict") + np.testing.assert_allclose(E_cli, E_ref, atol=atol, rtol=rtol) + np.testing.assert_allclose(std_cli, std_ref, atol=atol, rtol=rtol) def test_mcmc_multi_rank_random(self, tmp_path): """Multiple MPI ranks with random data should match np.sum reference.""" @@ -638,8 +636,9 @@ def test_mcmc_multi_rank_random(self, tmp_path): m = re.search(r"E\s*=\s*([+-]?[\d.eE+-]+)\s*\+-\s*([\d.eE+-]+)", result.output) assert m is not None E_cli, std_cli = float(m.group(1)), float(m.group(2)) - np.testing.assert_allclose(E_cli, E_ref, atol=atol_consistency, rtol=rtol_consistency) - np.testing.assert_allclose(std_cli, std_ref, atol=atol_consistency, rtol=rtol_consistency) + atol, rtol = get_tolerance("io", "strict") + np.testing.assert_allclose(E_cli, E_ref, atol=atol, rtol=rtol) + np.testing.assert_allclose(std_cli, std_ref, atol=atol, rtol=rtol) def test_mcmc_warmup_discards_steps(self, tmp_path): """Warmup discard + random data post-warmup must match reference.""" @@ -669,8 +668,9 @@ def test_mcmc_warmup_discards_steps(self, tmp_path): m = re.search(r"E\s*=\s*([+-]?[\d.eE+-]+)\s*\+-\s*([\d.eE+-]+)", result.output) assert m is not None E_cli, std_cli = float(m.group(1)), float(m.group(2)) - np.testing.assert_allclose(E_cli, E_ref, atol=atol_consistency, rtol=rtol_consistency) - np.testing.assert_allclose(std_cli, std_ref, atol=atol_consistency, rtol=rtol_consistency) + atol, rtol = get_tolerance("io", "strict") + np.testing.assert_allclose(E_cli, E_ref, atol=atol, rtol=rtol) + np.testing.assert_allclose(std_cli, std_ref, atol=atol, rtol=rtol) def test_lrdmc_random_energy_jackknife(self, tmp_path): """LRDMC jqmc-tool must match the np.sum-based reference jackknife for random data.""" @@ -699,8 +699,9 @@ def test_lrdmc_random_energy_jackknife(self, tmp_path): m = re.search(r"E\s*=\s*([+-]?[\d.eE+-]+)\s*\+-\s*([\d.eE+-]+)", result.output) assert m is not None E_cli, std_cli = float(m.group(1)), float(m.group(2)) - np.testing.assert_allclose(E_cli, E_ref, atol=atol_consistency, rtol=rtol_consistency) - np.testing.assert_allclose(std_cli, std_ref, atol=atol_consistency, rtol=rtol_consistency) + atol, rtol = get_tolerance("io", "strict") + np.testing.assert_allclose(E_cli, E_ref, atol=atol, rtol=rtol) + np.testing.assert_allclose(std_cli, std_ref, atol=atol, rtol=rtol) def test_lrdmc_multi_rank_random(self, tmp_path): """Multiple LRDMC ranks with random data must match np.sum reference.""" @@ -730,8 +731,9 @@ def test_lrdmc_multi_rank_random(self, tmp_path): m = re.search(r"E\s*=\s*([+-]?[\d.eE+-]+)\s*\+-\s*([\d.eE+-]+)", result.output) assert m is not None E_cli, std_cli = float(m.group(1)), float(m.group(2)) - np.testing.assert_allclose(E_cli, E_ref, atol=atol_consistency, rtol=rtol_consistency) - np.testing.assert_allclose(std_cli, std_ref, atol=atol_consistency, rtol=rtol_consistency) + atol, rtol = get_tolerance("io", "strict") + np.testing.assert_allclose(E_cli, E_ref, atol=atol, rtol=rtol) + np.testing.assert_allclose(std_cli, std_ref, atol=atol, rtol=rtol) def test_lrdmc_extrapolate_energy(self, tmp_path): """extrapolate-energy with two LRDMC checkpoints should report a->0 result.""" diff --git a/tests/test_lrdmc_force.py b/tests/test_lrdmc_force.py index c2cafd2a..3ee6e7d4 100755 --- a/tests/test_lrdmc_force.py +++ b/tests/test_lrdmc_force.py @@ -44,7 +44,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import atol_debug_vs_production, rtol_debug_vs_production # noqa: E402 +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -204,21 +204,22 @@ def test_lrdmc_force_with_SWCT_n(trexio_file: str, jastrow_parameters: dict, loc ) # See [J. Chem. Phys. 156, 034101 (2022)] + atol, rtol = get_tolerance("gfmc", "strict") assert not np.any(np.isnan(np.asarray(np.array(force_mean[0])))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(-1.0 * np.array(force_mean[1])))), "NaN detected in second argument" np.testing.assert_allclose( np.array(force_mean[0]), -1.0 * np.array(force_mean[1]), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.array(force_std[0])))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.array(force_std[1])))), "NaN detected in second argument" np.testing.assert_allclose( np.array(force_std[0]), np.array(force_std[1]), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) @@ -315,21 +316,22 @@ def test_lrdmc_force_with_SWCT_t(trexio_file: str, jastrow_parameters: dict, loc ) # See [J. Chem. Phys. 156, 034101 (2022)] + atol, rtol = get_tolerance("gfmc", "strict") assert not np.any(np.isnan(np.asarray(np.array(force_mean[0])))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(-1.0 * np.array(force_mean[1])))), "NaN detected in second argument" np.testing.assert_allclose( np.array(force_mean[0]), -1.0 * np.array(force_mean[1]), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.array(force_std[0])))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.array(force_std[1])))), "NaN detected in second argument" np.testing.assert_allclose( np.array(force_std[0]), np.array(force_std[1]), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) diff --git a/tests/test_mcmc_force.py b/tests/test_mcmc_force.py index 1fd1eddc..8bba9bee 100755 --- a/tests/test_mcmc_force.py +++ b/tests/test_mcmc_force.py @@ -44,7 +44,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import atol_debug_vs_production, rtol_debug_vs_production # noqa: E402 +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -199,21 +199,22 @@ def test_mcmc_force_with_SWCT(trexio_file: str, jastrow_parameters: dict): ) # See [J. Chem. Phys. 156, 034101 (2022)] + atol, rtol = get_tolerance("mcmc", "strict") assert not np.any(np.isnan(np.asarray(np.array(force_mean[0])))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(-1.0 * np.array(force_mean[1])))), "NaN detected in second argument" np.testing.assert_allclose( np.array(force_mean[0]), -1.0 * np.array(force_mean[1]), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(np.array(force_std[0])))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.array(force_std[1])))), "NaN detected in second argument" np.testing.assert_allclose( np.array(force_std[0]), np.array(force_std[1]), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) diff --git a/tests/test_structure.py b/tests/test_structure.py index f927660f..7a5ced36 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from jqmc._setting import atol_debug_vs_production, rtol_debug_vs_production +from jqmc._precision import get_tolerance from jqmc.structure import ( Structure_data, _find_nearest_index_jnp, @@ -54,6 +54,7 @@ def _make_non_pbc_structure(): def test_reciprocal_lattice_dot_2pi(): """Test that the dot product of the cell and reciprocal cell gives 2pi delta_ij.""" + atol, rtol = get_tolerance("io", "strict") structure = _make_pbc_structure() recip = structure.recip_cell cell = structure.cell @@ -63,11 +64,12 @@ def test_reciprocal_lattice_dot_2pi(): expected = 2.0 * np.pi if i == j else 0.0 assert not np.any(np.isnan(np.asarray(dot))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(expected))), "NaN detected in second argument" - np.testing.assert_allclose(dot, expected, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(dot, expected, atol=atol, rtol=rtol) def test_np_jnp_consistency_non_pbc(): """Test consistency between NumPy and JAX implementations for non-PBC structures.""" + atol, rtol = get_tolerance("io", "strict") structure = _make_non_pbc_structure() r_cart = np.array([0.2, 0.0, 0.0]) @@ -76,8 +78,8 @@ def test_np_jnp_consistency_non_pbc(): np.testing.assert_allclose( structure._positions_cart_np, np.asarray(structure._positions_cart_jnp), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) idx_np = _find_nearest_index_np(structure, r_cart) @@ -93,11 +95,12 @@ def test_np_jnp_consistency_non_pbc(): rel_jnp = np.asarray(_get_min_dist_rel_R_cart_jnp(structure, r_cart, i_atom)) assert not np.any(np.isnan(np.asarray(rel_np))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(rel_jnp))), "NaN detected in second argument" - np.testing.assert_allclose(rel_np, rel_jnp, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(rel_np, rel_jnp, atol=atol, rtol=rtol) def test_pbc_minimum_image_and_nearest(): """Test PBC minimum image convention and nearest nucleus finding.""" + atol, rtol = get_tolerance("io", "strict") structure = _make_pbc_structure() r_cart = np.array([9.1, 0.0, 0.0]) @@ -117,31 +120,32 @@ def test_pbc_minimum_image_and_nearest(): np.testing.assert_allclose( rel_atom0, np.array([0.9, 0.0, 0.0]), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(rel_atom1))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.array([-0.1, 0.0, 0.0])))), "NaN detected in second argument" np.testing.assert_allclose( rel_atom1, np.array([-0.1, 0.0, 0.0]), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) rel_atom0_jnp = np.asarray(_get_min_dist_rel_R_cart_jnp(structure, r_cart, 0)) rel_atom1_jnp = np.asarray(_get_min_dist_rel_R_cart_jnp(structure, r_cart, 1)) assert not np.any(np.isnan(np.asarray(rel_atom0_jnp))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(rel_atom0))), "NaN detected in second argument" - np.testing.assert_allclose(rel_atom0_jnp, rel_atom0, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(rel_atom0_jnp, rel_atom0, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(rel_atom1_jnp))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(rel_atom1))), "NaN detected in second argument" - np.testing.assert_allclose(rel_atom1_jnp, rel_atom1, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(rel_atom1_jnp, rel_atom1, atol=atol, rtol=rtol) @pytest.mark.parametrize("use_pbc", [False, True]) def test_find_nearest_index_matches_min_dist_jnp(use_pbc): """Test that the nearest index found matches the minimum distance calculation.""" + atol, rtol = get_tolerance("io", "strict") structure = _make_pbc_structure() if use_pbc else _make_non_pbc_structure() r_cart = np.array([9.1, 0.0, 0.0]) if use_pbc else np.array([1.8, 0.1, 0.0]) @@ -160,4 +164,4 @@ def test_find_nearest_index_matches_min_dist_jnp(use_pbc): assert not np.any(np.isnan(np.asarray(dist_idx))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.min(dist_all)))), "NaN detected in second argument" - np.testing.assert_allclose(dist_idx, np.min(dist_all), atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(dist_idx, np.min(dist_all), atol=atol, rtol=rtol) diff --git a/tests/test_swct.py b/tests/test_swct.py index a7cb5bd0..6bd1b397 100755 --- a/tests/test_swct.py +++ b/tests/test_swct.py @@ -44,10 +44,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import ( # noqa: E402 - atol_debug_vs_production, - rtol_debug_vs_production, -) +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.swct import ( # noqa: E402 _evaluate_swct_domega_debug, _evaluate_swct_omega_debug, @@ -64,6 +61,7 @@ @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5"]) def test_debug_and_jax_SWCT_omega(trexio_file: str): """Test SWCT omega, compare debug and jax.""" + atol, rtol = get_tolerance("kinetic", "strict") ( structure_data, _, @@ -88,10 +86,10 @@ def test_debug_and_jax_SWCT_omega(trexio_file: str): assert not np.any(np.isnan(np.asarray(omega_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(omega_up_jax))), "NaN detected in second argument" - np.testing.assert_allclose(omega_up_debug, omega_up_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(omega_up_debug, omega_up_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(omega_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(omega_dn_jax))), "NaN detected in second argument" - np.testing.assert_allclose(omega_dn_debug, omega_dn_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(omega_dn_debug, omega_dn_jax, atol=atol, rtol=rtol) domega_up_debug = _evaluate_swct_domega_debug(structure_data=structure_data, r_carts=r_up_carts) domega_dn_debug = _evaluate_swct_domega_debug(structure_data=structure_data, r_carts=r_dn_carts) @@ -100,10 +98,10 @@ def test_debug_and_jax_SWCT_omega(trexio_file: str): assert not np.any(np.isnan(np.asarray(domega_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(domega_up_jax))), "NaN detected in second argument" - np.testing.assert_allclose(domega_up_debug, domega_up_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(domega_up_debug, domega_up_jax, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(domega_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(domega_dn_jax))), "NaN detected in second argument" - np.testing.assert_allclose(domega_dn_debug, domega_dn_jax, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(domega_dn_debug, domega_dn_jax, atol=atol, rtol=rtol) jax.clear_caches() diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index 87021042..c81d7874 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -45,14 +45,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._setting import ( # noqa: E402 - atol_auto_vs_analytic_deriv, - atol_auto_vs_numerical_deriv, - atol_debug_vs_production, - rtol_auto_vs_analytic_deriv, - rtol_auto_vs_numerical_deriv, - rtol_debug_vs_production, -) +from jqmc._precision import get_tolerance # noqa: E402 from jqmc.determinant import compute_geminal_all_elements # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -87,6 +80,7 @@ @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) def test_kinetic_energy_analytic_and_numerical(trexio_file: str): """Test the kinetic energy computation.""" @@ -127,13 +121,14 @@ def test_kinetic_energy_analytic_and_numerical(trexio_file: str): K_debug = _compute_kinetic_energy_debug(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) K_jax = compute_kinetic_energy(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) + atol, rtol = get_tolerance("kinetic", "loose") assert not np.any(np.isnan(np.asarray(np.asarray(K_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(K_jax)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(K_debug), np.asarray(K_jax), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol, + atol=atol, ) @@ -177,9 +172,10 @@ def test_kinetic_energy_analytic_and_auto(trexio_file: str): r_dn_carts=jnp.asarray(r_dn_carts), ) + atol, rtol = get_tolerance("kinetic", "strict") assert not np.any(np.isnan(np.asarray(K_analytic))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_auto))), "NaN detected in second argument" - np.testing.assert_allclose(K_analytic, K_auto, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv) + np.testing.assert_allclose(K_analytic, K_auto, atol=atol, rtol=rtol) @pytest.mark.activate_if_skip_heavy @@ -225,24 +221,21 @@ def test_debug_and_auto_kinetic_energy_all_elements(trexio_file: str): wavefunction_data=wavefunction_data, r_up_carts=r_up_carts_jnp, r_dn_carts=r_dn_carts_jnp ) + atol, rtol = get_tolerance("kinetic", "loose") assert not np.any(np.isnan(np.asarray(K_elements_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_up_auto))), "NaN detected in second argument" - np.testing.assert_allclose( - K_elements_up_debug, K_elements_up_auto, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(K_elements_up_debug, K_elements_up_auto, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(K_elements_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_dn_auto))), "NaN detected in second argument" - np.testing.assert_allclose( - K_elements_dn_debug, K_elements_dn_auto, atol=atol_auto_vs_numerical_deriv, rtol=rtol_auto_vs_numerical_deriv - ) + np.testing.assert_allclose(K_elements_dn_debug, K_elements_dn_auto, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(np.asarray(K_elements_up_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(K_elements_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(K_elements_up_debug), np.asarray(K_elements_up_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol, + atol=atol, ) assert not np.any(np.isnan(np.asarray(np.asarray(K_elements_dn_debug)))), "NaN detected in first argument" @@ -250,8 +243,8 @@ def test_debug_and_auto_kinetic_energy_all_elements(trexio_file: str): np.testing.assert_allclose( np.asarray(K_elements_dn_debug), np.asarray(K_elements_dn_auto), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol, + atol=atol, ) @@ -298,16 +291,13 @@ def test_auto_and_analytic_kinetic_energy_all_elements(trexio_file: str): wavefunction_data=wavefunction_data, r_up_carts=r_up_carts_jnp, r_dn_carts=r_dn_carts_jnp ) + atol, rtol = get_tolerance("kinetic", "strict") assert not np.any(np.isnan(np.asarray(K_elements_up_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_up_analytic))), "NaN detected in second argument" - np.testing.assert_allclose( - K_elements_up_auto, K_elements_up_analytic, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(K_elements_up_auto, K_elements_up_analytic, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(K_elements_dn_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_dn_analytic))), "NaN detected in second argument" - np.testing.assert_allclose( - K_elements_dn_auto, K_elements_dn_analytic, atol=atol_auto_vs_analytic_deriv, rtol=rtol_auto_vs_analytic_deriv - ) + np.testing.assert_allclose(K_elements_dn_auto, K_elements_dn_analytic, atol=atol, rtol=rtol) @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) @@ -369,12 +359,13 @@ def test_fast_update_kinetic_energy_all_elements(trexio_file: str): geminal_inverse=A_inv, ) + atol, rtol = get_tolerance("kinetic", "strict") assert not np.any(np.isnan(np.asarray(ke_up_fast))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(ke_up_debug))), "NaN detected in second argument" - np.testing.assert_allclose(ke_up_fast, ke_up_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(ke_up_fast, ke_up_debug, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(ke_dn_fast))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(ke_dn_debug))), "NaN detected in second argument" - np.testing.assert_allclose(ke_dn_fast, ke_dn_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production) + np.testing.assert_allclose(ke_dn_fast, ke_dn_debug, atol=atol, rtol=rtol) @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) @@ -450,50 +441,49 @@ def test_debug_and_jax_discretized_kinetic_energy(trexio_file: str): RT=RT, ) + atol, rtol = get_tolerance("kinetic", "strict") assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_up_carts_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_up_carts_debug))), "NaN detected in second argument" np.testing.assert_allclose( mesh_kinetic_part_r_up_carts_jax, mesh_kinetic_part_r_up_carts_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_dn_carts_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_dn_carts_debug))), "NaN detected in second argument" np.testing.assert_allclose( mesh_kinetic_part_r_dn_carts_jax, mesh_kinetic_part_r_dn_carts_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_up_carts_jax_fast_update))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_up_carts_debug))), "NaN detected in second argument" np.testing.assert_allclose( mesh_kinetic_part_r_up_carts_jax_fast_update, mesh_kinetic_part_r_up_carts_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_dn_carts_jax_fast_update))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_dn_carts_debug))), "NaN detected in second argument" np.testing.assert_allclose( mesh_kinetic_part_r_dn_carts_jax_fast_update, mesh_kinetic_part_r_dn_carts_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) assert not np.any(np.isnan(np.asarray(elements_kinetic_part_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(elements_kinetic_part_debug))), "NaN detected in second argument" - np.testing.assert_allclose( - elements_kinetic_part_jax, elements_kinetic_part_debug, atol=atol_debug_vs_production, rtol=rtol_debug_vs_production - ) + np.testing.assert_allclose(elements_kinetic_part_jax, elements_kinetic_part_debug, atol=atol, rtol=rtol) assert not np.any(np.isnan(np.asarray(elements_kinetic_part_jax_fast_update))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(elements_kinetic_part_debug))), "NaN detected in second argument" np.testing.assert_allclose( elements_kinetic_part_jax_fast_update, elements_kinetic_part_debug, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, ) @@ -552,11 +542,12 @@ def test_nodal_distance_analytic_vs_debug(trexio_file: str): ) # They should be identical up to numerical noise + atol, rtol = get_tolerance("kinetic", "loose") np.testing.assert_allclose( np.asarray(nd_analytic), np.asarray(nd_debug), - rtol=rtol_auto_vs_numerical_deriv, - atol=atol_auto_vs_numerical_deriv, + rtol=rtol, + atol=atol, ) # Sanity: nodal distance should be positive @@ -604,6 +595,7 @@ def test_evaluate_ln_wavefunction_fast_forward(trexio_file): n_up = geminal_data.num_electron_up n_dn = geminal_data.num_electron_dn + atol, rtol = get_tolerance("kinetic", "strict") for _ in range(10): r_up = jnp.array(rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) @@ -618,8 +610,8 @@ def test_evaluate_ln_wavefunction_fast_forward(trexio_file): np.testing.assert_allclose( val_fast, val_ref, - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, err_msg=f"Forward mismatch: fast={val_fast:.15f}, ref={val_ref:.15f}", ) @@ -640,6 +632,7 @@ def test_evaluate_ln_wavefunction_fast_backward(trexio_file): grad_ref_fn = jax.grad(evaluate_ln_wavefunction, argnums=0) grad_fast_fn = jax.grad(evaluate_ln_wavefunction_fast, argnums=0) + atol, rtol = get_tolerance("kinetic", "strict") for _ in range(10): r_up = jnp.array(rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) @@ -653,8 +646,8 @@ def test_evaluate_ln_wavefunction_fast_backward(trexio_file): lambda a, b: np.testing.assert_allclose( np.asarray(a), np.asarray(b), - atol=atol_debug_vs_production, - rtol=rtol_debug_vs_production, + atol=atol, + rtol=rtol, err_msg="Backward mismatch in evaluate_ln_wavefunction_fast", ), grad_ref, From dc517409418c4e6d2046ee7c282d7a21ef0b2794 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:35:17 +0900 Subject: [PATCH 02/97] Add explicit `dtype=get_dtype("gfmc")` to float-typed local arrays in jqmc/jqmc_gfmc.py to remove implicit reliance on JAX dtype propagation. Behavior is unchanged (gfmc zone defaults to float64 in both full/mixed modes); this improves precision-zone clarity and resilience against future JAX promotion-rule changes. --- jqmc/jqmc_gfmc.py | 71 ++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 0bc30685..adefa34c 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -704,7 +704,8 @@ def _generate_rotation_matrix_t(alpha, beta, gamma): [cos_b * cos_g, cos_g * sin_a * sin_b - cos_a * sin_g, sin_a * sin_g + cos_a * cos_g * sin_b], [cos_b * sin_g, cos_a * cos_g + sin_a * sin_b * sin_g, cos_a * sin_b * sin_g - cos_g * sin_a], [-sin_b, cos_b * sin_a, cos_a * cos_b], - ] + ], + dtype=get_dtype("gfmc"), ) return R @@ -1547,10 +1548,10 @@ def _compute_local_energy_t( _n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up _n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn _n_atoms = self.__hamiltonian_data.structure_data.natom - omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up)) - omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn)) - grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3)) - grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3)) + omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=get_dtype("gfmc")) + omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=get_dtype("gfmc")) + grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) + grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) end_observable = time.perf_counter() timer_observable += end_observable - start_observable @@ -1942,8 +1943,8 @@ def _compute_local_energy_t( self.__num_survived_walkers += num_survived_walkers self.__num_killed_walkers += num_killed_walkers self.__stored_average_projection_counter[self.__mcmc_counter + num_mcmc_done] = ave_projection_counter - self.__latest_r_up_carts = jnp.array(latest_r_up_carts_after_branching) - self.__latest_r_dn_carts = jnp.array(latest_r_dn_carts_after_branching) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=get_dtype("gfmc")) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=get_dtype("gfmc")) self.__latest_A_old_inv = vmap(_compute_initial_A_inv_t, in_axes=(0, 0))( self.__latest_r_up_carts, self.__latest_r_dn_carts ) @@ -3019,7 +3020,7 @@ def _compute_local_energy_t_debug( # Always set the initial weight list to 1.0 projection_counter_list = jnp.array([0 for _ in range(self.__num_walkers)]) - e_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)]) + e_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) logger.devel(" Projection is on going....") @@ -3327,10 +3328,10 @@ def _compute_local_energy_t_debug( # projection ends projection_counter_list = jnp.array(projection_counter_list) - e_L_list = jnp.array(e_L_list) - w_L_list = jnp.array(w_L_list) - self.__latest_r_up_carts = jnp.array(latest_r_up_carts) - self.__latest_r_dn_carts = jnp.array(latest_r_dn_carts) + e_L_list = jnp.asarray(e_L_list, dtype=get_dtype("gfmc")) + w_L_list = jnp.asarray(w_L_list, dtype=get_dtype("gfmc")) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts, dtype=get_dtype("gfmc")) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts, dtype=get_dtype("gfmc")) self.__jax_PRNG_key_list = jnp.array(jax_PRNG_key_list) logger.debug(" Projection ends.") @@ -3429,10 +3430,10 @@ def _compute_local_energy_t_debug( _n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up _n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn _n_atoms = self.__hamiltonian_data.structure_data.natom - omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up)) - omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn)) - grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3)) - grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3)) + omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=get_dtype("gfmc")) + omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=get_dtype("gfmc")) + grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) + grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) # jnp.array -> np.array w_L_latest = np.array(w_L_list) @@ -3627,8 +3628,8 @@ def _compute_local_energy_t_debug( self.__num_survived_walkers += num_survived_walkers self.__num_killed_walkers += num_killed_walkers self.__stored_average_projection_counter.append(ave_projection_counter) - self.__latest_r_up_carts = jnp.array(latest_r_up_carts_after_branching) - self.__latest_r_dn_carts = jnp.array(latest_r_dn_carts_after_branching) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=get_dtype("gfmc")) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=get_dtype("gfmc")) # count up, here is the end of the branching step. num_mcmc_done += 1 @@ -4402,7 +4403,8 @@ def _generate_rotation_matrix_n(alpha, beta, gamma): [cos_b * cos_g, cos_g * sin_a * sin_b - cos_a * sin_g, sin_a * sin_g + cos_a * cos_g * sin_b], [cos_b * sin_g, cos_a * cos_g + sin_a * sin_b * sin_g, cos_a * sin_b * sin_g - cos_g * sin_a], [-sin_b, cos_b * sin_a, cos_a * cos_b], - ] + ], + dtype=get_dtype("gfmc"), ) return R @@ -5416,10 +5418,10 @@ def _compute_local_energy_n( _n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up _n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn _n_atoms = self.__hamiltonian_data.structure_data.natom - omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up)) - omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn)) - grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3)) - grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3)) + omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=get_dtype("gfmc")) + omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=get_dtype("gfmc")) + grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) + grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) # Barrier before MPI operation start_mpi_barrier = time.perf_counter() @@ -5804,8 +5806,8 @@ def _compute_local_energy_n( # here update the walker positions!! self.__num_survived_walkers += num_survived_walkers self.__num_killed_walkers += num_killed_walkers - self.__latest_r_up_carts = jnp.array(latest_r_up_carts_after_branching) - self.__latest_r_dn_carts = jnp.array(latest_r_dn_carts_after_branching) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=get_dtype("gfmc")) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=get_dtype("gfmc")) self.__latest_A_old_inv = _jit_vmap_A_inv_n(self.__latest_r_up_carts, self.__latest_r_dn_carts) mpi_comm.Barrier() @@ -6732,7 +6734,8 @@ def _generate_rotation_matrix_n_debug(alpha, beta, gamma): [cos_b * cos_g, cos_g * sin_a * sin_b - cos_a * sin_g, sin_a * sin_g + cos_a * cos_g * sin_b], [cos_b * sin_g, cos_a * cos_g + sin_a * sin_b * sin_g, cos_a * sin_b * sin_g - cos_g * sin_a], [-sin_b, cos_b * sin_a, cos_a * cos_b], - ] + ], + dtype=get_dtype("gfmc"), ) return R @@ -7445,10 +7448,10 @@ def _compute_local_energy_n_debug( _n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up _n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn _n_atoms = self.__hamiltonian_data.structure_data.natom - omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up)) - omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn)) - grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3)) - grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3)) + omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=get_dtype("gfmc")) + omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=get_dtype("gfmc")) + grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) + grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) # jnp.array -> np.array w_L_latest = np.array(w_L_list) @@ -7653,8 +7656,8 @@ def _compute_local_energy_n_debug( # here update the walker positions!! self.__num_survived_walkers += num_survived_walkers self.__num_killed_walkers += num_killed_walkers - self.__latest_r_up_carts = jnp.array(latest_r_up_carts_after_branching) - self.__latest_r_dn_carts = jnp.array(latest_r_dn_carts_after_branching) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=get_dtype("gfmc")) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=get_dtype("gfmc")) # update E_scf eq_steps = GFMC_ON_THE_FLY_WARMUP_STEPS @@ -7691,8 +7694,8 @@ def get_E_on_the_fly( # logger.info(f" (w_L_eq) = {(w_L_eq)}") logger.devel(" Progress: Computing G_eq and G_e_L_eq.") - w_L_eq = jnp.array(w_L_eq) - e_L_eq = jnp.array(e_L_eq) + w_L_eq = jnp.asarray(w_L_eq, dtype=get_dtype("gfmc")) + e_L_eq = jnp.asarray(e_L_eq, dtype=get_dtype("gfmc")) G_eq = _compute_G_L_debug(w_L_eq, num_gfmc_collect_steps) G_e_L_eq = e_L_eq * G_eq G_eq = np.array(G_eq) From f270f2de2d76776a8801b75167a4199864f26753 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 24 Apr 2026 22:39:08 +0900 Subject: [PATCH 03/97] perf(mixed-precision): seal fp64 leaks in zone boundaries (Phase 9) Implement zone-aware fp32/fp64 dtype propagation across the orb_eval / jastrow / geminal / coulomb (fp32 in mixed mode) and determinant / kinetic / wavefunction / IO (fp64) precision zones, so that mode=mixed actually delivers the intended speedup over mode=full instead of being collapsed back to fp64 by JAX type promotion. Implementation - atomic_orbital.py: cast AO data fields and r_carts at zone entry (incl. _atomic_center_carts_prim_jnp and friends; covers both cart and sphe paths). - molecular_orbital.py: cast mo_coefficients at zone entry. - jastrow_factor.py: cast J1/J2/J3 r_carts and variational params to the jastrow zone dtype; J3-NN params handled via tree_map. - determinant.py: cast geminal inputs / lambda blocks to the geminal zone dtype, including the MCMC fast-update row/column kernels. - coulomb_potential.py: - cast inputs at zone entry across the bare/local/non-local ECP paths (~15 np.array sites). - cast wf_ratio_up / wf_ratio_dn back to the coulomb zone in compute_ecp_non_local_part_all_pairs_jax_weights_grid_points, sealing fp64 leak from determinant/jastrow into V_ecp pytree. - structure.py: mark dtype as static for _get_min_dist_rel_R_cart_jnp (@partial(jit, static_argnames=("dtype",)))-required because Python scalar types like jnp.float32 cannot be traced as abstract arrays. --- jqmc/atomic_orbital.py | 102 ++++--- jqmc/coulomb_potential.py | 31 +- jqmc/determinant.py | 12 +- jqmc/jastrow_factor.py | 56 ++-- jqmc/molecular_orbital.py | 24 +- jqmc/structure.py | 15 +- tests/test_mixed_precision.py | 549 ++++++++++++++++++++++++++++++++++ 7 files changed, 696 insertions(+), 93 deletions(-) create mode 100644 tests/test_mixed_precision.py diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index ce61982f..29106081 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -58,7 +58,7 @@ from ._jqmc_utility import _spherical_to_cart_matrix from ._precision import get_dtype -from ._setting import EPS_stabilizing_jax_AO_cart_deriv, atol_consistency, rtol_consistency +from ._setting import EPS_stabilizing_jax_AO_cart_deriv, atol_consistency, get_eps, rtol_consistency from .structure import Structure_data # set logger @@ -1986,16 +1986,18 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A See compute_AOs_api """ - # Indices with respect to the contracted AOs - R_carts_jnp = aos_data._atomic_center_carts_prim_jnp - c_jnp = aos_data._coefficients_jnp - Z_jnp = aos_data._exponents_jnp + # Downcast all float inputs to orb_eval zone dtype (P0-1, P0-2) + dtype = get_dtype("orb_eval") + r_carts = r_carts.astype(dtype) + R_carts_jnp = aos_data._atomic_center_carts_prim_jnp.astype(dtype) + c_jnp = aos_data._coefficients_jnp.astype(dtype) + Z_jnp = aos_data._exponents_jnp.astype(dtype) l_jnp = aos_data._angular_momentums_prim_jnp nx_jnp = aos_data._polynominal_order_x_prim_jnp ny_jnp = aos_data._polynominal_order_y_prim_jnp nz_jnp = aos_data._polynominal_order_z_prim_jnp - N_n_dup_fuctorial_part = aos_data._normalization_factorial_ratio_prim_jnp + N_n_dup_fuctorial_part = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype) N_n_dup_Z_part = (2.0 * Z_jnp / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z_jnp) ** l_jnp N_n_dup = jnp.sqrt(N_n_dup_Z_part * N_n_dup_fuctorial_part) r_R_diffs = r_carts[None, :, :] - R_carts_jnp[:, None, :] @@ -2003,7 +2005,7 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) x, y, z = r_R_diffs[..., 0], r_R_diffs[..., 1], r_R_diffs[..., 2] - eps = EPS_stabilizing_jax_AO_cart_deriv # This is quite important to avoid some numerical instability in JAX!! + eps = get_eps("stabilizing_ao", dtype) P_l_nx_ny_nz_dup = (x + eps) ** (nx_jnp[:, None]) * (y + eps) ** (ny_jnp[:, None]) * (z + eps) ** (nz_jnp[:, None]) """ @@ -2033,26 +2035,27 @@ def _compute_AOs_sphe(aos_data: AOs_sphe_data, r_carts: jnpt.ArrayLike) -> jax.A See compute_AOs_api """ - # Indices with respect to the contracted AOs - # compute R_n inc. the whole normalization factor + # Downcast all float inputs to orb_eval zone dtype (P0-1) + dtype = get_dtype("orb_eval") + r_carts = r_carts.astype(dtype) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp - R_carts_jnp = aos_data._atomic_center_carts_prim_jnp - R_carts_unique_jnp = aos_data._atomic_center_carts_unique_jnp - c_jnp = aos_data._coefficients_jnp - Z_jnp = aos_data._exponents_jnp + R_carts_jnp = aos_data._atomic_center_carts_prim_jnp.astype(dtype) + R_carts_unique_jnp = aos_data._atomic_center_carts_unique_jnp.astype(dtype) + c_jnp = aos_data._coefficients_jnp.astype(dtype) + Z_jnp = aos_data._exponents_jnp.astype(dtype) l_jnp = aos_data._angular_momentums_prim_jnp m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp - # use float64 gamma-based factorials to avoid float32 drift vs debug implementation - l_f64 = l_jnp.astype(jnp.float64) - Z_f64 = Z_jnp.astype(jnp.float64) - factorial_l_plus_1 = jnp.exp(jscipy.special.gammaln(l_f64 + 2.0)) - factorial_2l_plus_2 = jnp.exp(jscipy.special.gammaln(2.0 * l_f64 + 3.0)) + # Normalization constants computed in zone dtype (P0-1: replaces .astype(jnp.float64)) + l_typed = l_jnp.astype(dtype) + factorial_l_plus_1 = jnp.exp(jscipy.special.gammaln(l_typed + 2.0)) + factorial_2l_plus_2 = jnp.exp(jscipy.special.gammaln(2.0 * l_typed + 3.0)) N_n_dup = jnp.sqrt( - (2.0 ** (2 * l_f64 + 3) * factorial_l_plus_1 * (2 * Z_f64) ** (l_f64 + 1.5)) / (factorial_2l_plus_2 * jnp.sqrt(jnp.pi)) + (2.0 ** (2 * l_typed + 3) * factorial_l_plus_1 * (2 * Z_jnp) ** (l_typed + 1.5)) + / (factorial_2l_plus_2 * jnp.sqrt(jnp.asarray(jnp.pi, dtype=dtype))) ) - N_l_m_dup = jnp.sqrt((2 * l_f64 + 1) / (4 * jnp.pi)) + N_l_m_dup = jnp.sqrt((2 * l_typed + 1) / (4 * jnp.asarray(jnp.pi, dtype=dtype))) r_R_diffs = r_carts[None, :, :] - R_carts_jnp[:, None, :] r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) @@ -2565,24 +2568,25 @@ def _single_val_grad_lap(diff: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.A def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for Cartesian AOs (contracted).""" dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts) - R_carts = aos_data._atomic_center_carts_prim_jnp - c = aos_data._coefficients_jnp - Z = aos_data._exponents_jnp + r_carts = jnp.asarray(r_carts, dtype=dtype) + R_carts = aos_data._atomic_center_carts_prim_jnp.astype(dtype) + c = aos_data._coefficients_jnp.astype(dtype) + Z = aos_data._exponents_jnp.astype(dtype) l = aos_data._angular_momentums_prim_jnp nx = aos_data._polynominal_order_x_prim_jnp ny = aos_data._polynominal_order_y_prim_jnp nz = aos_data._polynominal_order_z_prim_jnp - N_fact = aos_data._normalization_factorial_ratio_prim_jnp + N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype) N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z) ** l N = jnp.sqrt(N_Z * N_fact) diff = r_carts[None, :, :] - R_carts[:, None, :] x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] - x = x + EPS_stabilizing_jax_AO_cart_deriv - y = y + EPS_stabilizing_jax_AO_cart_deriv - z = z + EPS_stabilizing_jax_AO_cart_deriv + eps = get_eps("stabilizing_ao", dtype) + x = x + eps + y = y + eps + z = z + eps r2 = jnp.sum(diff**2, axis=-1) pref = c[:, None] * jnp.exp(-Z[:, None] * r2) @@ -2610,12 +2614,12 @@ def _second_component(base, n): def _compute_AOs_laplacian_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for spherical AOs (contracted).""" dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts) + r_carts = jnp.asarray(r_carts, dtype=dtype) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp - R_carts_jnp = aos_data._atomic_center_carts_prim_jnp - R_carts_unique_jnp = aos_data._atomic_center_carts_unique_jnp - c_jnp = aos_data._coefficients_jnp - Z_jnp = aos_data._exponents_jnp + R_carts_jnp = aos_data._atomic_center_carts_prim_jnp.astype(dtype) + R_carts_unique_jnp = aos_data._atomic_center_carts_unique_jnp.astype(dtype) + c_jnp = aos_data._coefficients_jnp.astype(dtype) + Z_jnp = aos_data._exponents_jnp.astype(dtype) l_jnp = aos_data._angular_momentums_prim_jnp m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp @@ -2794,7 +2798,8 @@ def _compute_AOs_laplacian_autodiff(aos_data: AOs_sphe_data | AOs_cart_data, r_c See compute_AOs_laplacian_api """ - dtype = get_dtype("kinetic") # noqa: F841 + dtype = get_dtype("kinetic") + r_carts = jnp.asarray(r_carts, dtype=dtype) # not very fast, but it works. ao_matrix_hessian = hessian(compute_AOs, argnums=1)(aos_data, r_carts) ao_matrix_laplacian = jnp.einsum("m i i u i u -> mi", ao_matrix_hessian) @@ -2870,25 +2875,26 @@ def _compute_AOs_laplacian_debug( @jit def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for Cartesian AOs (contracted).""" - dtype = get_dtype("kinetic") # noqa: F841 - r_carts = jnp.asarray(r_carts) - R_carts = aos_data._atomic_center_carts_prim_jnp - c = aos_data._coefficients_jnp - Z = aos_data._exponents_jnp + dtype = get_dtype("kinetic") + r_carts = jnp.asarray(r_carts, dtype=dtype) + R_carts = aos_data._atomic_center_carts_prim_jnp.astype(dtype) + c = aos_data._coefficients_jnp.astype(dtype) + Z = aos_data._exponents_jnp.astype(dtype) l = aos_data._angular_momentums_prim_jnp nx = aos_data._polynominal_order_x_prim_jnp ny = aos_data._polynominal_order_y_prim_jnp nz = aos_data._polynominal_order_z_prim_jnp - N_fact = aos_data._normalization_factorial_ratio_prim_jnp + N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype) N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z) ** l N = jnp.sqrt(N_Z * N_fact) diff = r_carts[None, :, :] - R_carts[:, None, :] x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] - x = x + EPS_stabilizing_jax_AO_cart_deriv - y = y + EPS_stabilizing_jax_AO_cart_deriv - z = z + EPS_stabilizing_jax_AO_cart_deriv + eps = get_eps("stabilizing_ao", dtype) + x = x + eps + y = y + eps + z = z + eps r2 = jnp.sum(diff**2, axis=-1) pref = c[:, None] * jnp.exp(-Z[:, None] * r2) @@ -2919,12 +2925,12 @@ def _grad_component(base, n): def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for spherical AOs (contracted).""" dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts) + r_carts = jnp.asarray(r_carts, dtype=dtype) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp - R_carts_jnp = aos_data._atomic_center_carts_prim_jnp - R_carts_unique_jnp = aos_data._atomic_center_carts_unique_jnp - c_jnp = aos_data._coefficients_jnp - Z_jnp = aos_data._exponents_jnp + R_carts_jnp = aos_data._atomic_center_carts_prim_jnp.astype(dtype) + R_carts_unique_jnp = aos_data._atomic_center_carts_unique_jnp.astype(dtype) + c_jnp = aos_data._coefficients_jnp.astype(dtype) + Z_jnp = aos_data._exponents_jnp.astype(dtype) l_jnp = aos_data._angular_momentums_prim_jnp m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 254771f2..8300ee40 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -1167,6 +1167,7 @@ def compute_V_l(r_cart, i_atom, exponent, coefficient, power): structure_data=coulomb_potential_data.structure_data, r_cart=r_cart, i_atom=i_atom, + dtype=dtype, ) V_l = ( jnp.linalg.norm(rel_R_cart_min_dist) ** -2.0 @@ -1213,10 +1214,11 @@ def compute_V_local( r_up_carts_jnp = jnp.asarray(r_up_carts, dtype=dtype) r_dn_carts_jnp = jnp.asarray(r_dn_carts, dtype=dtype) + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 i_atom_np = np.array(coulomb_potential_data._nucleus_index_local_part) - exponent_np = np.array(coulomb_potential_data._exponents_local_part) - coefficient_np = np.array(coulomb_potential_data._coefficients_local_part) - power_np = np.array(coulomb_potential_data._powers_local_part) + exponent_np = np.array(coulomb_potential_data._exponents_local_part, dtype=dtype_np) + coefficient_np = np.array(coulomb_potential_data._coefficients_local_part, dtype=dtype_np) + power_np = np.array(coulomb_potential_data._powers_local_part, dtype=dtype_np) V_ecp_up = jnp.sum( vmap_vmap_compute_ecp_up( @@ -1349,6 +1351,7 @@ def _rels_for_electron(r_cart, i_atom_list): structure_data=coulomb_potential_data.structure_data, r_cart=r_cart, i_atom=i_atom, + dtype=dtype, ) )(i_atom_list) @@ -1576,6 +1579,7 @@ def _rels_for_electron(r_cart, i_atom_list): structure_data=coulomb_potential_data.structure_data, r_cart=r_cart, i_atom=i_atom, + dtype=dtype, ) )(i_atom_list) @@ -1859,6 +1863,7 @@ def compute_V_l(r_cart, i_atom, exponent, coefficient, power): structure_data=coulomb_potential_data.structure_data, r_cart=r_cart, i_atom=i_atom, + dtype=dtype, ) V_l = ( jnp.linalg.norm(rel_R_cart_min_dist) ** -2.0 @@ -1877,6 +1882,7 @@ def compute_P_l_up(ang_mom, r_up_i, r_up_cart, i_atom, weight, vec_delta): structure_data=coulomb_potential_data.structure_data, r_cart=r_up_cart, i_atom=i_atom, + dtype=dtype, ) r_up_carts_on_mesh = r_up_carts r_up_carts_on_mesh = r_up_carts_on_mesh.at[r_up_i].set( @@ -1897,6 +1903,9 @@ def compute_P_l_up(ang_mom, r_up_i, r_up_cart, i_atom, weight, vec_delta): det_numerator_up = compute_det_geminal_all_elements(wavefunction_data.geminal_data, r_up_carts_on_mesh, r_dn_carts) wf_ratio_up = jnp.exp(jastrow_numerator_up - jastrow_denominator) * det_numerator_up / det_denominator + # Cast back to coulomb zone: det/jastrow live in fp64 zones and would otherwise + # promote the entire P_l / V_ecp output to fp64. + wf_ratio_up = jnp.asarray(wf_ratio_up, dtype=dtype) P_l_up = (2 * ang_mom + 1) * jnp_legendre_tablated(ang_mom, cos_theta_up) * weight * wf_ratio_up @@ -1910,6 +1919,7 @@ def compute_P_l_dn(ang_mom, r_dn_i, r_dn_cart, i_atom, weight, vec_delta): structure_data=coulomb_potential_data.structure_data, r_cart=r_dn_cart, i_atom=i_atom, + dtype=dtype, ) r_dn_carts_on_mesh = r_dn_carts r_dn_carts_on_mesh = r_dn_carts_on_mesh.at[r_dn_i].set( @@ -1930,6 +1940,8 @@ def compute_P_l_dn(ang_mom, r_dn_i, r_dn_cart, i_atom, weight, vec_delta): det_numerator_dn = compute_det_geminal_all_elements(wavefunction_data.geminal_data, r_up_carts, r_dn_carts_on_mesh) wf_ratio_dn = jnp.exp(jastrow_numerator_dn - jastrow_denominator) * det_numerator_dn / det_denominator + # Cast back to coulomb zone (see compute_P_l_up). + wf_ratio_dn = jnp.asarray(wf_ratio_dn, dtype=dtype) P_l_dn = (2 * ang_mom + 1) * jnp_legendre_tablated(ang_mom, cos_theta_dn) * weight * wf_ratio_dn return r_dn_carts_on_mesh, P_l_dn @@ -2005,9 +2017,9 @@ def compute_V_nonlocal_dn( i_atom_np = jnp.array(coulomb_potential_data._nucleus_index_non_local_part) ang_mom_np = jnp.array(coulomb_potential_data._ang_mom_non_local_part) - exponent_np = jnp.array(coulomb_potential_data._exponents_non_local_part) - coefficient_np = jnp.array(coulomb_potential_data._coefficients_non_local_part) - power_np = jnp.array(coulomb_potential_data._powers_non_local_part) + exponent_np = jnp.array(coulomb_potential_data._exponents_non_local_part, dtype=dtype) + coefficient_np = jnp.array(coulomb_potential_data._coefficients_non_local_part, dtype=dtype) + power_np = jnp.array(coulomb_potential_data._powers_non_local_part, dtype=dtype) r_up_carts_on_mesh, V_ecp_up = vmap_vmap_compute_ecp_up( r_up_i_jnp, @@ -2242,7 +2254,7 @@ def compute_bare_coulomb_potential_el_ion_element_wise( dtype = get_dtype("coulomb") dtype_np = np.float64 if dtype == jnp.float64 else np.float32 R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype) - R_charges = np.array(coulomb_potential_data._effective_charges) + R_charges = np.array(coulomb_potential_data._effective_charges, dtype=dtype_np) r_up_charges = np.full(len(r_up_carts), -1.0, dtype=dtype_np) r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=dtype_np) @@ -2287,7 +2299,7 @@ def compute_discretized_bare_coulomb_potential_el_ion_element_wise( dtype = get_dtype("coulomb") dtype_np = np.float64 if dtype == jnp.float64 else np.float32 R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype) - R_charges = np.array(coulomb_potential_data._effective_charges) + R_charges = np.array(coulomb_potential_data._effective_charges, dtype=dtype_np) r_up_charges = np.full(len(r_up_carts), -1.0, dtype=dtype_np) r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=dtype_np) @@ -2457,8 +2469,9 @@ def compute_bare_coulomb_potential_ion_ion( float: Ion–ion Coulomb energy. """ dtype = get_dtype("coulomb") + dtype_np = np.float64 if dtype == jnp.float64 else np.float32 R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype) - R_charges = np.array(coulomb_potential_data._effective_charges) + R_charges = np.array(coulomb_potential_data._effective_charges, dtype=dtype_np) all_charges = R_charges all_carts = R_carts diff --git a/jqmc/determinant.py b/jqmc/determinant.py index 5be2211d..11d6fd2f 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -1392,14 +1392,16 @@ def _compute_geminal_all_elements_debug( @jax.jit def compute_geminal_up_one_row_elements( geminal_data, - r_up_cart: jax.Array, # shape: (3,) or (1,3) + r_up_cart: jax.Array, # shape: (1, 3) r_dn_carts: jax.Array, # shape: (N_dn, 3) ) -> jax.Array: """Single row of the geminal matrix for one spin-up electron. Args: geminal_data: Geminal parameters and orbital references. - r_up_cart: Cartesian coordinate for one spin-up electron with shape ``(3,)`` or ``(1, 3)``. + r_up_cart: Cartesian coordinate for one spin-up electron with shape ``(1, 3)``. + ``compute_orb_api`` requires a 2D ``(N, 3)`` batch; pass a single + electron as ``(1, 3)``, not ``(3,)``. r_dn_carts: Cartesian coordinates for all spin-down electrons with shape ``(N_dn, 3)``. Returns: @@ -1440,14 +1442,16 @@ def compute_geminal_up_one_row_elements( def compute_geminal_dn_one_column_elements( geminal_data, r_up_carts: jax.Array, # shape: (N_up, 3) - r_dn_cart: jax.Array, # shape: (3,) or (1,3) + r_dn_cart: jax.Array, # shape: (1, 3) ) -> jax.Array: """Single column of the geminal matrix for one spin-down electron. Args: geminal_data: Geminal parameters and orbital references. r_up_carts: Cartesian coordinates of spin-up electrons with shape ``(N_up, 3)``. - r_dn_cart: Cartesian coordinate for one spin-down electron with shape ``(3,)`` or ``(1, 3)``. + r_dn_cart: Cartesian coordinate for one spin-down electron with shape ``(1, 3)``. + ``compute_orb_api`` requires a 2D ``(N, 3)`` batch; pass a single + electron as ``(1, 3)``, not ``(3,)``. Returns: jax.Array: Column vector for the paired block with shape ``(N_up,)``. diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index ba46d567..91ae2a2d 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -656,11 +656,14 @@ def compute_Jastrow_one_body( float: One-body Jastrow value (before exponentiation). """ dtype = get_dtype("jastrow") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) # Retrieve structure data and convert to JAX arrays R_carts = jnp.array(jastrow_one_body_data.structure_data.positions, dtype=dtype) atomic_numbers = jnp.array(jastrow_one_body_data.structure_data.atomic_numbers, dtype=dtype) core_electrons = jnp.array(jastrow_one_body_data.core_electrons, dtype=dtype) effective_charges = atomic_numbers - core_electrons + j1b = jnp.asarray(jastrow_one_body_data.jastrow_1b_param, dtype=dtype) j1b_type = jastrow_one_body_data.jastrow_1b_type @@ -676,7 +679,6 @@ def one_body_jastrow_kernel( return 1.0 / (2.0 * param) * (1.0 - jnp.exp(-param * coeff * jnp.linalg.norm(r_cart - R_cart))) def atom_contrib(r_cart, R_cart, Z_eff): - j1b = jastrow_one_body_data.jastrow_1b_param coeff = (2.0 * Z_eff) ** (1.0 / 4.0) return -((2.0 * Z_eff) ** (3.0 / 4.0)) * one_body_jastrow_kernel(j1b, coeff, r_cart, R_cart) @@ -684,7 +686,6 @@ def atom_contrib(r_cart, R_cart, Z_eff): def atom_contrib(r_cart, R_cart, Z_eff): """Pade form of J1: -Z_eff^{3/4} * r_eN / (2*(1 + a * Z_eff^{1/4} * r_eN)).""" - j1b = jastrow_one_body_data.jastrow_1b_param r_eN = jnp.linalg.norm(r_cart - R_cart) coeff = (2.0 * Z_eff) ** (1.0 / 4.0) return -((2.0 * Z_eff) ** (3.0 / 4.0)) * r_eN / (2.0 * (1.0 + j1b * coeff * r_eN)) @@ -1105,6 +1106,10 @@ def compute_Jastrow_two_body( Returns: float: Two-body Jastrow value (before exponentiation). """ + dtype_j2 = get_dtype("jastrow") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype_j2) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype_j2) + j2b_param = jnp.asarray(jastrow_two_body_data.jastrow_2b_param, dtype=dtype_j2) j2b_type = jastrow_two_body_data.jastrow_2b_type def two_body_jastrow_exp(param: float, r_cart_i: jnpt.ArrayLike, r_cart_j: jnpt.ArrayLike) -> float: @@ -1128,18 +1133,14 @@ def two_body_jastrow_pade(param: float, r_cart_i: jnpt.ArrayLike, r_cart_j: jnpt vmap(two_body_jastrow_anti_parallel, in_axes=(None, None, 0)), in_axes=(None, 0, None) ) - two_body_jastrow_anti_parallel_val = jnp.sum( - vmap_two_body_jastrow_anti_parallel_spins(jastrow_two_body_data.jastrow_2b_param, r_up_carts, r_dn_carts) - ) + two_body_jastrow_anti_parallel_val = jnp.sum(vmap_two_body_jastrow_anti_parallel_spins(j2b_param, r_up_carts, r_dn_carts)) def compute_parallel_sum(r_carts): num_particles = r_carts.shape[0] idx_i, idx_j = jnp.triu_indices(num_particles, k=1) r_i = r_carts[idx_i] r_j = r_carts[idx_j] - vmap_two_body_jastrow_parallel_spins = vmap(two_body_jastrow_parallel, in_axes=(None, 0, 0))( - jastrow_two_body_data.jastrow_2b_param, r_i, r_j - ) + vmap_two_body_jastrow_parallel_spins = vmap(two_body_jastrow_parallel, in_axes=(None, 0, 0))(j2b_param, r_i, r_j) return jnp.sum(vmap_two_body_jastrow_parallel_spins) two_body_jastrow_parallel_up = compute_parallel_sum(r_up_carts) @@ -1656,6 +1657,9 @@ def compute_Jastrow_three_body( float: Three-body Jastrow value (before exponentiation). """ dtype = get_dtype("jastrow") + r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) + r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + j_matrix = jastrow_three_body_data.j_matrix.astype(dtype) num_electron_up = len(r_up_carts) num_electron_dn = len(r_dn_carts) @@ -1665,11 +1669,11 @@ def compute_Jastrow_three_body( K_up = jnp.tril(jnp.ones((num_electron_up, num_electron_up), dtype=dtype), k=-1) K_dn = jnp.tril(jnp.ones((num_electron_dn, num_electron_dn), dtype=dtype), k=-1) - j1_matrix_up = jastrow_three_body_data.j_matrix[:, -1] - j1_matrix_dn = jastrow_three_body_data.j_matrix[:, -1] - j3_matrix_up_up = jastrow_three_body_data.j_matrix[:, :-1] - j3_matrix_dn_dn = jastrow_three_body_data.j_matrix[:, :-1] - j3_matrix_up_dn = jastrow_three_body_data.j_matrix[:, :-1] + j1_matrix_up = j_matrix[:, -1] + j1_matrix_dn = j_matrix[:, -1] + j3_matrix_up_up = j_matrix[:, :-1] + j3_matrix_dn_dn = j_matrix[:, :-1] + j3_matrix_up_dn = j_matrix[:, :-1] e_up = jnp.ones(num_electron_up, dtype=dtype).T e_dn = jnp.ones(num_electron_dn, dtype=dtype).T @@ -2060,8 +2064,11 @@ def compute_Jastrow_part(jastrow_data: Jastrow_data, r_up_carts: jax.Array, r_dn raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") R_n = jnp.asarray(nn3.structure_data.positions, dtype=dtype) - Z_n = jnp.asarray(nn3.structure_data.atomic_numbers) - J3_nn = nn3.nn_def.apply({"params": nn3.params}, r_up_carts, r_dn_carts, R_n, Z_n) + Z_n = jnp.asarray(nn3.structure_data.atomic_numbers, dtype=dtype) + nn_params = jax.tree_util.tree_map( + lambda x: x.astype(dtype) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params + ) + J3_nn = nn3.nn_def.apply({"params": nn_params}, r_up_carts, r_dn_carts, R_n, Z_n) J3 = J3 + J3_nn J = J1 + J2 + J3 @@ -2101,8 +2108,11 @@ def _compute_Jastrow_part_debug( Z_n = np.asarray(nn3.structure_data.atomic_numbers, dtype=dtype_np) # Use JAX NN for debug as well; convert inputs to jnp and back to float + nn_params = jax.tree_util.tree_map( + lambda x: x.astype(dtype) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params + ) J3_nn = nn3.nn_def.apply( - {"params": nn3.params}, + {"params": nn_params}, jnp.asarray(r_up_carts, dtype=dtype), jnp.asarray(r_dn_carts, dtype=dtype), jnp.asarray(R_n, dtype=dtype), @@ -2854,10 +2864,13 @@ def compute_grads_and_laplacian_Jastrow_part( r_up_carts_jnp = jnp.asarray(r_up_carts, dtype=dtype) r_dn_carts_jnp = jnp.asarray(r_dn_carts, dtype=dtype) R_n = jnp.asarray(nn3.structure_data.positions, dtype=dtype) - Z_n = jnp.asarray(nn3.structure_data.atomic_numbers) + Z_n = jnp.asarray(nn3.structure_data.atomic_numbers, dtype=dtype) def _compute_Jastrow_nn_only(r_up, r_dn): - return nn3.nn_def.apply({"params": nn3.params}, r_up, r_dn, R_n, Z_n) + nn_params = jax.tree_util.tree_map( + lambda x: x.astype(dtype) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params + ) + return nn3.nn_def.apply({"params": nn_params}, r_up, r_dn, R_n, Z_n) grad_JNN_up = grad(_compute_Jastrow_nn_only, argnums=0)(r_up_carts_jnp, r_dn_carts_jnp) grad_JNN_dn = grad(_compute_Jastrow_nn_only, argnums=1)(r_up_carts_jnp, r_dn_carts_jnp) @@ -2962,10 +2975,13 @@ def _compute_grads_and_laplacian_Jastrow_part_auto( raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") R_n = jnp.asarray(nn3.structure_data.positions, dtype=dtype) - Z_n = jnp.asarray(nn3.structure_data.atomic_numbers) + Z_n = jnp.asarray(nn3.structure_data.atomic_numbers, dtype=dtype) def _compute_Jastrow_nn_only(r_up, r_dn): - return nn3.nn_def.apply({"params": nn3.params}, r_up, r_dn, R_n, Z_n) + nn_params = jax.tree_util.tree_map( + lambda x: x.astype(dtype) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params + ) + return nn3.nn_def.apply({"params": nn_params}, r_up, r_dn, R_n, Z_n) grad_JNN_up = grad(_compute_Jastrow_nn_only, argnums=0)(r_up_carts_jnp, r_dn_carts_jnp) grad_JNN_dn = grad(_compute_Jastrow_nn_only, argnums=1)(r_up_carts_jnp, r_dn_carts_jnp) diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index e8de77cb..c9fc822d 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -232,8 +232,10 @@ def compute_MOs(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: Returns: jax.Array: MO values with shape ``(num_mo, N_e)``. """ + dtype = get_dtype("orb_eval") + mo_coefficients = mos_data.mo_coefficients.astype(dtype) answer = jnp.dot( - mos_data.mo_coefficients, + mo_coefficients, compute_AOs(aos_data=mos_data.aos_data, r_carts=r_carts), ) @@ -271,8 +273,10 @@ def compute_MOs_laplacian(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: Returns: jax.Array: Laplacians of each MO, shape ``(num_mo, N_e)``. """ + dtype = get_dtype("kinetic") + mo_coefficients = mos_data.mo_coefficients.astype(dtype) ao_lap = compute_AOs_laplacian(mos_data.aos_data, r_carts) - return jnp.dot(mos_data.mo_coefficients, ao_lap) + return jnp.dot(mo_coefficients, ao_lap) def _compute_MOs_laplacian_debug(mos_data: MOs_data, r_carts: npt.NDArray[np.float64]): @@ -333,10 +337,12 @@ def compute_MOs_grad( tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64]]: Gradients per component ``(grad_x, grad_y, grad_z)``, each of shape ``(num_mo, N_e)``. """ + dtype = get_dtype("kinetic") + mo_coefficients = mos_data.mo_coefficients.astype(dtype) mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z = compute_AOs_grad(mos_data.aos_data, r_carts) - mo_matrix_grad_x = jnp.dot(mos_data.mo_coefficients, mo_matrix_grad_x) - mo_matrix_grad_y = jnp.dot(mos_data.mo_coefficients, mo_matrix_grad_y) - mo_matrix_grad_z = jnp.dot(mos_data.mo_coefficients, mo_matrix_grad_z) + mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x) + mo_matrix_grad_y = jnp.dot(mo_coefficients, mo_matrix_grad_y) + mo_matrix_grad_z = jnp.dot(mo_coefficients, mo_matrix_grad_z) return mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z @@ -350,10 +356,12 @@ def _compute_MOs_grad_autodiff( npt.NDArray[np.float64], ]: """This method is for computing the gradients (x,y,z) of the given molecular orbital at r_carts.""" + dtype = get_dtype("kinetic") + mo_coefficients = mos_data.mo_coefficients.astype(dtype) mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z = _compute_AOs_grad_autodiff(mos_data.aos_data, r_carts) - mo_matrix_grad_x = jnp.dot(mos_data.mo_coefficients, mo_matrix_grad_x) - mo_matrix_grad_y = jnp.dot(mos_data.mo_coefficients, mo_matrix_grad_y) - mo_matrix_grad_z = jnp.dot(mos_data.mo_coefficients, mo_matrix_grad_z) + mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x) + mo_matrix_grad_y = jnp.dot(mo_coefficients, mo_matrix_grad_y) + mo_matrix_grad_z = jnp.dot(mo_coefficients, mo_matrix_grad_z) return mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z diff --git a/jqmc/structure.py b/jqmc/structure.py index 1c057546..ce31f73f 100755 --- a/jqmc/structure.py +++ b/jqmc/structure.py @@ -455,10 +455,17 @@ def _get_min_dist_rel_R_cart_np(structure_data: Structure_data, r_cart: list[flo return diff -@jit -def _get_min_dist_rel_R_cart_jnp(structure_data: Structure_data, r_cart: list[float, float, float], i_atom: int) -> float: - """See get_min_dist_rel_R_cart_np.""" - dtype = get_dtype("io") +@partial(jit, static_argnames=("dtype",)) +def _get_min_dist_rel_R_cart_jnp( + structure_data: Structure_data, r_cart: list[float, float, float], i_atom: int, dtype=None +) -> float: + """See get_min_dist_rel_R_cart_np. + + ``dtype`` is marked static for ``jit`` because it is a Python scalar type + (e.g. ``jnp.float32``) and cannot be traced as an abstract array. + """ + if dtype is None: + dtype = get_dtype("io") r_cart = jnp.array(r_cart, dtype=dtype) R_cart = jnp.array(structure_data._positions_cart_jnp[i_atom], dtype=dtype) diff = R_cart - r_cart diff --git a/tests/test_mixed_precision.py b/tests/test_mixed_precision.py new file mode 100644 index 00000000..92027e48 --- /dev/null +++ b/tests/test_mixed_precision.py @@ -0,0 +1,549 @@ +"""Mixed precision dtype propagation tests. + +These tests verify that when ``--precision-mode=mixed`` is active, each +Precision Zone produces outputs in the expected dtype. They catch JAX +dtype-promotion bugs where fp64 data (io/optimization zone) leaks into +fp32 compute kernels and silently promotes the entire computation to fp64. + +Every test: + 1. Configures ``mode="mixed"`` explicitly (independent of the CLI flag). + 2. Calls the target function with realistic inputs. + 3. Asserts the output dtype matches the zone's configured dtype. + +In ``mode="full"`` (the default), all zones are fp64 and these tests are +trivially satisfied, so they are skipped to save time. + +Run with:: + + pytest tests/test_mixed_precision.py -v --precision-mode=mixed +""" + +import os +import sys +from pathlib import Path + +import jax +import jax.numpy as jnp +import numpy as np +import pytest + +project_root = str(Path(__file__).parent.parent) +if project_root not in sys.path: + sys.path.insert(0, project_root) + +from jqmc._precision import configure, get_dtype # noqa: E402 +from jqmc.atomic_orbital import ( # noqa: E402 + compute_AOs, + compute_AOs_grad, + compute_AOs_laplacian, +) +from jqmc.coulomb_potential import ( # noqa: E402 + compute_bare_coulomb_potential, + compute_bare_coulomb_potential_el_el, + compute_bare_coulomb_potential_el_ion_element_wise, + compute_ecp_local_parts_all_pairs, + compute_ecp_non_local_part_all_pairs_jax_weights_grid_points, +) +from jqmc.determinant import ( # noqa: E402 + compute_geminal_all_elements, + compute_geminal_dn_one_column_elements, + compute_geminal_up_one_row_elements, + compute_ln_det_geminal_all_elements, +) +from jqmc.hamiltonians import Hamiltonian_data, compute_local_energy # noqa: E402 +from jqmc.jastrow_factor import ( # noqa: E402 + Jastrow_data, + Jastrow_one_body_data, + Jastrow_two_body_data, + Jastrow_three_body_data, + compute_Jastrow_one_body, + compute_Jastrow_part, + compute_Jastrow_two_body, + compute_Jastrow_three_body, +) +from jqmc.molecular_orbital import ( # noqa: E402 + compute_MOs, + compute_MOs_grad, + compute_MOs_laplacian, +) +from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 +from jqmc.wavefunction import ( # noqa: E402 + Wavefunction_data, + compute_kinetic_energy, + evaluate_ln_wavefunction, +) + +# JAX float64 +jax.config.update("jax_enable_x64", True) +jax.config.update("jax_traceback_filtering", "off") + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +TREXIO_DIR = os.path.join(os.path.dirname(__file__), "trexio_example_files") + + +@pytest.fixture(autouse=True) +def _skip_if_not_mixed(request): + """Skip these tests unless --precision-mode=mixed is active.""" + if request.config.getoption("--precision-mode") != "mixed": + pytest.skip("Only runs with --precision-mode=mixed") + + +@pytest.fixture(autouse=True) +def _configure_mixed(): + """Ensure mixed mode is active and JIT caches are cleared.""" + configure({"mode": "mixed"}) + jax.clear_caches() + yield + # Restore full mode after each test to avoid polluting other tests + configure({"mode": "full"}) + jax.clear_caches() + + +def _load_trexio(filename: str) -> dict: + """Helper that loads a TREXIO file and synthesizes random electron coordinates.""" + trexio_file = os.path.join(TREXIO_DIR, filename) + structure_data, aos_data, mos_data_up, mos_data_dn, geminal_data, coulomb_data = read_trexio_file( + trexio_file=trexio_file, store_tuple=True + ) + rng = np.random.default_rng(42) + n_up = geminal_data.num_electron_up + n_dn = geminal_data.num_electron_dn + r_up = jnp.array(rng.standard_normal((n_up, 3)) * 0.5, dtype=jnp.float64) + r_dn = jnp.array(rng.standard_normal((n_dn, 3)) * 0.5, dtype=jnp.float64) + return { + "structure_data": structure_data, + "aos_data": aos_data, + "mos_data_up": mos_data_up, + "mos_data_dn": mos_data_dn, + "geminal_data": geminal_data, + "coulomb_data": coulomb_data, + "r_up": r_up, + "r_dn": r_dn, + } + + +@pytest.fixture +def h2_data(): + """Load H2 all-electron Cartesian basis test data.""" + return _load_trexio("H2_ae_ccpvdz_cart.h5") + + +@pytest.fixture +def h2_sphe_data(): + """Load H2 all-electron spherical basis test data (covers AO_sphe path).""" + return _load_trexio("H2_ae_ccpvdz_sphe.h5") + + +@pytest.fixture +def h2_ecp_data(): + """Load H2 ECP test data (covers ECP local/non-local paths).""" + return _load_trexio("H2_ecp_ccpvtz.h5") + + +def _assert_dtype(arr, expected, label): + """Helper: assert array (or scalar) dtype matches expected.""" + actual = jnp.asarray(arr).dtype + assert actual == expected, ( + f"{label} dtype is {actual}, expected {expected}. " + "Check kernel-entry casts of (1) input r_carts, (2) all pytree float fields, " + "(3) jnp.array/zeros/ones literals." + ) + + +def _assert_eval_shape_dtype(fn, expected, label, *args, **kwargs): + """Helper: use jax.eval_shape (no actual execution) to assert output dtype. + + Useful for heavy kernels (ECP non-local) where executing in mixed mode + is slow. Returns a ShapeDtypeStruct (or pytree thereof) and we walk the + leaves to assert every float leaf has the expected dtype. + """ + out = jax.eval_shape(fn, *args, **kwargs) + leaves = jax.tree_util.tree_leaves(out) + bad = [(i, leaf.dtype) for i, leaf in enumerate(leaves) if leaf.dtype.kind == "f" and leaf.dtype != expected] + assert not bad, ( + f"{label} has float leaves with unexpected dtype: {bad}. Expected {expected}. " + "Heavy kernels checked via jax.eval_shape (no execution)." + ) + + +# --------------------------------------------------------------------------- +# A. AO zone (orb_eval → float32 in mixed) +# --------------------------------------------------------------------------- + + +class TestAODtype: + """Verify AO evaluation outputs are float32 in mixed mode.""" + + def test_compute_AOs_output_dtype(self, h2_data): + """compute_AOs must return float32 (orb_eval zone).""" + AOs = compute_AOs(h2_data["aos_data"], h2_data["r_up"]) + expected = get_dtype("orb_eval") + assert AOs.dtype == expected, ( + f"compute_AOs output dtype is {AOs.dtype}, expected {expected}. " + "Likely cause: fp64 data (R_carts, exponents, coefficients) not cast to orb_eval dtype inside kernel." + ) + + def test_compute_AOs_grad_output_dtype(self, h2_data): + """compute_AOs_grad must return float in kinetic zone dtype.""" + grad_x, grad_y, grad_z = compute_AOs_grad(h2_data["aos_data"], h2_data["r_up"]) + expected = get_dtype("kinetic") + for name, arr in [("grad_x", grad_x), ("grad_y", grad_y), ("grad_z", grad_z)]: + assert arr.dtype == expected, f"compute_AOs_grad {name} dtype is {arr.dtype}, expected {expected}." + + def test_compute_AOs_laplacian_output_dtype(self, h2_data): + """compute_AOs_laplacian must return kinetic zone dtype.""" + lap = compute_AOs_laplacian(h2_data["aos_data"], h2_data["r_up"]) + expected = get_dtype("kinetic") + assert lap.dtype == expected, f"compute_AOs_laplacian dtype is {lap.dtype}, expected {expected}." + + +# --------------------------------------------------------------------------- +# B. MO zone (orb_eval → float32 in mixed) +# --------------------------------------------------------------------------- + + +class TestMODtype: + """Verify MO evaluation outputs are float32 in mixed mode.""" + + def test_compute_MOs_output_dtype(self, h2_data): + """compute_MOs must return float32 (orb_eval zone). + + Catches: mo_coefficients (fp64) × AOs (fp32) promotion. + """ + MOs = compute_MOs(h2_data["mos_data_up"], h2_data["r_up"]) + expected = get_dtype("orb_eval") + assert MOs.dtype == expected, ( + f"compute_MOs output dtype is {MOs.dtype}, expected {expected}. " + "Likely cause: mo_coefficients not cast to orb_eval dtype." + ) + + +# --------------------------------------------------------------------------- +# C. Jastrow zone (jastrow → float32 in mixed) +# --------------------------------------------------------------------------- + + +class TestJastrowDtype: + """Verify Jastrow outputs are float32 in mixed mode.""" + + def test_jastrow_two_body_output_dtype(self, h2_data): + """compute_Jastrow_two_body must return float32 (jastrow zone). + + Catches: jastrow_2b_param (fp64) not cast to jastrow dtype. + """ + j2_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="pade") + J2 = compute_Jastrow_two_body(j2_data, h2_data["r_up"], h2_data["r_dn"]) + expected = get_dtype("jastrow") + assert jnp.asarray(J2).dtype == expected, ( + f"compute_Jastrow_two_body dtype is {jnp.asarray(J2).dtype}, expected {expected}. " + "Likely cause: jastrow_2b_param not cast to jastrow dtype." + ) + + def test_jastrow_three_body_output_dtype(self, h2_data): + """compute_Jastrow_three_body must return float32 (jastrow zone). + + Catches: j_matrix (fp64) not cast to jastrow dtype. + """ + j3_data = Jastrow_three_body_data.init_jastrow_three_body_data( + orb_data=h2_data["aos_data"], random_init=True, random_scale=1e-3 + ) + J3 = compute_Jastrow_three_body(j3_data, h2_data["r_up"], h2_data["r_dn"]) + expected = get_dtype("jastrow") + assert jnp.asarray(J3).dtype == expected, ( + f"compute_Jastrow_three_body dtype is {jnp.asarray(J3).dtype}, expected {expected}. " + "Likely cause: j_matrix not cast to jastrow dtype." + ) + + def test_jastrow_part_output_dtype(self, h2_data): + """compute_Jastrow_part (J1+J2+J3) must return float32 (jastrow zone).""" + j2_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="exp") + j3_data = Jastrow_three_body_data.init_jastrow_three_body_data( + orb_data=h2_data["aos_data"], random_init=True, random_scale=1e-3 + ) + jastrow_data = Jastrow_data( + jastrow_one_body_data=None, + jastrow_two_body_data=j2_data, + jastrow_three_body_data=j3_data, + ) + J = compute_Jastrow_part(jastrow_data, h2_data["r_up"], h2_data["r_dn"]) + expected = get_dtype("jastrow") + assert jnp.asarray(J).dtype == expected, f"compute_Jastrow_part dtype is {jnp.asarray(J).dtype}, expected {expected}." + + +# --------------------------------------------------------------------------- +# D. Geminal zone (geminal → float32 in mixed) +# --------------------------------------------------------------------------- + + +class TestGeminalDtype: + """Verify geminal matrix is float32 in mixed mode.""" + + def test_geminal_matrix_output_dtype(self, h2_data): + """compute_geminal_all_elements must return float32 (geminal zone). + + Catches: lambda_matrix or AO data not cast to geminal dtype. + """ + G = compute_geminal_all_elements(h2_data["geminal_data"], h2_data["r_up"], h2_data["r_dn"]) + expected = get_dtype("geminal") + assert G.dtype == expected, f"compute_geminal_all_elements dtype is {G.dtype}, expected {expected}." + + +# --------------------------------------------------------------------------- +# E. Determinant zone (determinant → float64 in mixed) +# --------------------------------------------------------------------------- + + +class TestDeterminantDtype: + """Verify determinant outputs stay float64 in mixed mode.""" + + def test_ln_det_output_dtype(self, h2_data): + """compute_ln_det_geminal_all_elements must return float64 (determinant zone). + + Even though geminal matrix is float32, the log-det must be computed in float64. + """ + ln_det = compute_ln_det_geminal_all_elements(h2_data["geminal_data"], h2_data["r_up"], h2_data["r_dn"]) + expected = get_dtype("determinant") + assert jnp.asarray(ln_det).dtype == expected, ( + f"compute_ln_det dtype is {jnp.asarray(ln_det).dtype}, expected {expected}." + ) + + +# --------------------------------------------------------------------------- +# F. Coulomb zone (coulomb → float32 in mixed) +# --------------------------------------------------------------------------- + + +class TestCoulombDtype: + """Verify Coulomb outputs are float32 in mixed mode.""" + + def test_bare_coulomb_el_el_output_dtype(self, h2_data): + """Electron-electron Coulomb must return float32 (coulomb zone).""" + V = compute_bare_coulomb_potential_el_el(h2_data["r_up"], h2_data["r_dn"]) + expected = get_dtype("coulomb") + assert jnp.asarray(V).dtype == expected, ( + f"compute_bare_coulomb_potential_el_el dtype is {jnp.asarray(V).dtype}, expected {expected}." + ) + + def test_bare_coulomb_el_ion_output_dtype(self, h2_data): + """Electron-ion Coulomb must return float32 (coulomb zone). + + Catches: R_charges (fp64) or structure positions (fp64) not cast. + """ + V_up, V_dn = compute_bare_coulomb_potential_el_ion_element_wise( + h2_data["coulomb_data"], h2_data["r_up"], h2_data["r_dn"] + ) + expected = get_dtype("coulomb") + assert V_up.dtype == expected, ( + f"el_ion V_up dtype is {V_up.dtype}, expected {expected}. " + "Likely cause: R_charges or R_carts not cast to coulomb dtype." + ) + + def test_bare_coulomb_total_output_dtype(self, h2_data): + """Total bare Coulomb must return float32 (coulomb zone).""" + V = compute_bare_coulomb_potential(h2_data["coulomb_data"], h2_data["r_up"], h2_data["r_dn"]) + expected = get_dtype("coulomb") + assert jnp.asarray(V).dtype == expected, ( + f"compute_bare_coulomb_potential dtype is {jnp.asarray(V).dtype}, expected {expected}." + ) + + +# --------------------------------------------------------------------------- +# G. Kinetic zone (kinetic → float64 in mixed) +# --------------------------------------------------------------------------- + + +class TestKineticDtype: + """Verify kinetic energy stays float64 in mixed mode.""" + + def test_kinetic_energy_output_dtype(self, h2_data): + """compute_kinetic_energy must return float64 (kinetic zone).""" + wf_data = Wavefunction_data(geminal_data=h2_data["geminal_data"]) + T = compute_kinetic_energy(wf_data, h2_data["r_up"], h2_data["r_dn"]) + expected = get_dtype("kinetic") + assert jnp.asarray(T).dtype == expected, f"compute_kinetic_energy dtype is {jnp.asarray(T).dtype}, expected {expected}." + + +# --------------------------------------------------------------------------- +# H. Wavefunction zone boundary +# --------------------------------------------------------------------------- + + +class TestWavefunctionDtype: + """Verify wavefunction evaluation zone boundaries.""" + + def test_ln_wavefunction_output_dtype(self, h2_data): + """evaluate_ln_wavefunction must return float64 (determinant zone). + + The output combines Jastrow (float32) and log-det (float64), + cast to determinant zone dtype. + """ + j2_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="exp") + jastrow_data = Jastrow_data( + jastrow_one_body_data=None, + jastrow_two_body_data=j2_data, + jastrow_three_body_data=None, + ) + wf_data = Wavefunction_data(geminal_data=h2_data["geminal_data"], jastrow_data=jastrow_data) + ln_psi = evaluate_ln_wavefunction(wf_data, h2_data["r_up"], h2_data["r_dn"]) + expected = get_dtype("determinant") + assert jnp.asarray(ln_psi).dtype == expected, ( + f"evaluate_ln_wavefunction dtype is {jnp.asarray(ln_psi).dtype}, expected {expected}." + ) + + +# --------------------------------------------------------------------------- +# Extended coverage (a): additional boundary kernels +# --------------------------------------------------------------------------- + + +class TestAOSpheDtype: + """Verify AO spherical basis path also returns float32 (orb_eval).""" + + def test_compute_AOs_sphe_output_dtype(self, h2_sphe_data): + AOs = compute_AOs(h2_sphe_data["aos_data"], h2_sphe_data["r_up"]) + _assert_dtype(AOs, get_dtype("orb_eval"), "compute_AOs (sphe)") + + def test_compute_AOs_sphe_grad_output_dtype(self, h2_sphe_data): + gx, gy, gz = compute_AOs_grad(h2_sphe_data["aos_data"], h2_sphe_data["r_up"]) + for name, arr in [("grad_x", gx), ("grad_y", gy), ("grad_z", gz)]: + _assert_dtype(arr, get_dtype("kinetic"), f"compute_AOs_grad sphe {name}") + + def test_compute_AOs_sphe_laplacian_output_dtype(self, h2_sphe_data): + lap = compute_AOs_laplacian(h2_sphe_data["aos_data"], h2_sphe_data["r_up"]) + _assert_dtype(lap, get_dtype("kinetic"), "compute_AOs_laplacian (sphe)") + + +class TestMOExtendedDtype: + """MO derivative kernels (kinetic zone).""" + + def test_compute_MOs_grad_output_dtype(self, h2_data): + gx, gy, gz = compute_MOs_grad(h2_data["mos_data_up"], h2_data["r_up"]) + expected = get_dtype("kinetic") + for name, arr in [("grad_x", gx), ("grad_y", gy), ("grad_z", gz)]: + _assert_dtype(arr, expected, f"compute_MOs_grad {name}") + + def test_compute_MOs_laplacian_output_dtype(self, h2_data): + lap = compute_MOs_laplacian(h2_data["mos_data_up"], h2_data["r_up"]) + _assert_dtype(lap, get_dtype("kinetic"), "compute_MOs_laplacian") + + +class TestJastrowOneBodyDtype: + """Verify J1 forward standalone output (jastrow zone).""" + + def test_compute_Jastrow_one_body_output_dtype(self, h2_data): + core_electrons = tuple([0.0] * len(h2_data["structure_data"].positions)) + j1_data = Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=1.0, + structure_data=h2_data["structure_data"], + core_electrons=core_electrons, + jastrow_1b_type="pade", + ) + J1 = compute_Jastrow_one_body(j1_data, h2_data["r_up"], h2_data["r_dn"]) + _assert_dtype(J1, get_dtype("jastrow"), "compute_Jastrow_one_body") + + +class TestGeminalFastUpdateDtype: + """Verify Geminal row/column kernels used in MCMC fast updates (geminal zone).""" + + @pytest.fixture + def water_data(self): + """Water ECP data with multiple electrons (needed for row/column tests).""" + return _load_trexio("water_ccecp_ccpvqz.h5") + + def test_geminal_up_one_row_output_dtype(self, water_data): + # Use [0:1] to get shape (1, 3) — compute_orb_api requires (N, 3), not (3,) + row = compute_geminal_up_one_row_elements(water_data["geminal_data"], water_data["r_up"][0:1], water_data["r_dn"]) + _assert_dtype(row, get_dtype("geminal"), "compute_geminal_up_one_row_elements") + + def test_geminal_dn_one_column_output_dtype(self, water_data): + # Use [0:1] to get shape (1, 3) — compute_orb_api requires (N, 3), not (3,) + col = compute_geminal_dn_one_column_elements(water_data["geminal_data"], water_data["r_up"], water_data["r_dn"][0:1]) + _assert_dtype(col, get_dtype("geminal"), "compute_geminal_dn_one_column_elements") + + +class TestECPDtype: + """Verify ECP local + non-local paths (coulomb zone). + + Local: executed directly. Non-local: heavy, checked via jax.eval_shape (no run). + """ + + def test_compute_ecp_local_parts_output_dtype(self, h2_ecp_data): + V_loc = compute_ecp_local_parts_all_pairs(h2_ecp_data["coulomb_data"], h2_ecp_data["r_up"], h2_ecp_data["r_dn"]) + _assert_dtype(V_loc, get_dtype("coulomb"), "compute_ecp_local_parts_all_pairs") + + def test_compute_ecp_non_local_eval_shape_dtype(self, h2_ecp_data): + """Heavy ECP non-local kernel: verify dtype via jax.eval_shape (no execution).""" + wf_data = Wavefunction_data(geminal_data=h2_ecp_data["geminal_data"]) + # Minimal Lebedev-like quadrature placeholder (6-point). Values are not + # used by eval_shape; only shapes/dtypes matter for static dtype tracing. + weights = jnp.ones((6,), dtype=jnp.float64) / 6.0 + grid_points = jnp.array( + [ + [1.0, 0.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, -1.0, 0.0], + [0.0, 0.0, 1.0], + [0.0, 0.0, -1.0], + ], + dtype=jnp.float64, + ) + _assert_eval_shape_dtype( + compute_ecp_non_local_part_all_pairs_jax_weights_grid_points, + get_dtype("coulomb"), + "compute_ecp_non_local_part_all_pairs_jax_weights_grid_points", + h2_ecp_data["coulomb_data"], + wf_data, + h2_ecp_data["r_up"], + h2_ecp_data["r_dn"], + weights, + grid_points, + ) + + +class TestLocalEnergyDtype: + """Final boundary: compute_local_energy aggregates kinetic + coulomb (kinetic zone).""" + + def test_compute_local_energy_output_dtype(self, h2_data): + wf_data = Wavefunction_data(geminal_data=h2_data["geminal_data"]) + ham = Hamiltonian_data( + structure_data=h2_data["structure_data"], + wavefunction_data=wf_data, + coulomb_potential_data=h2_data["coulomb_data"], + ) + RT = jnp.eye(3, dtype=jnp.float64) + e_L = compute_local_energy(ham, h2_data["r_up"], h2_data["r_dn"], RT) + _assert_dtype(e_L, get_dtype("kinetic"), "compute_local_energy") + + +class TestKineticEvalShape: + """jax.eval_shape coverage for kinetic energy fast/discretized variants. + + These exercise different code paths (Sherman-Morrison fast update, + discretized DMC) without actually executing the heavy autodiff laplacian. + """ + + def test_kinetic_energy_eval_shape_dtype(self, h2_data): + wf_data = Wavefunction_data(geminal_data=h2_data["geminal_data"]) + _assert_eval_shape_dtype( + compute_kinetic_energy, + get_dtype("kinetic"), + "compute_kinetic_energy (eval_shape)", + wf_data, + h2_data["r_up"], + h2_data["r_dn"], + ) + + def test_evaluate_ln_wavefunction_eval_shape_dtype(self, h2_data): + wf_data = Wavefunction_data(geminal_data=h2_data["geminal_data"]) + _assert_eval_shape_dtype( + evaluate_ln_wavefunction, + get_dtype("determinant"), + "evaluate_ln_wavefunction (eval_shape)", + wf_data, + h2_data["r_up"], + h2_data["r_dn"], + ) From 39fb607038b010688aa1b96b09d324935ee5ae99 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 24 Apr 2026 23:21:28 +0900 Subject: [PATCH 04/97] Implementation fixes: - jqmc_mcmc.py (_update_electron_positions): cast lax.cond branches in v / u construction to geminal_inv.dtype. The geminal-diff branch lives in the geminal zone (fp64) while jax.nn.one_hot defaults to fp32, so in mixed precision the cond branches disagreed on dtype. - jqmc_mcmc.py (_update_electron_positions_only_up_electron): cast the Sherman-Morrison rank-1 inverse update back to geminal_inv.dtype so _accepted_fun and _rejected_fun branches of lax.cond agree. - jqmc_gfmc.py (debug-vs-production e_L check at the tmove branch): replace hard-coded `assert_almost_equal(..., decimal=6)` with `assert_allclose(..., atol, rtol)` driven by `get_tolerance_min` over the e_L path zones. The previous hard-coded decimal silently ignored the surrounding `rtol_debug_vs_production` setting and broke under mixed precision. Test tolerance fixes: - tests/test_jqmc_mcmc.py, tests/test_jqmc_gfmc_tau.py, tests/test_jqmc_gfmc_bra.py: switch e_L / e_L2 / w_L / ln|Psi| / H_0/f/S/K/B comparisons from get_tolerance("mcmc"/"gfmc", "strict") to get_tolerance_min(, "strict"). The mcmc/gfmc zones are fp64-pinned even in mixed mode, but the e_L computation path crosses orb_eval/jastrow/geminal/coulomb/kinetic which are fp32 in mixed mode, so tolerances must be bounded by the weakest zone. --- jqmc/determinant.py | 7 +++- jqmc/jqmc_gfmc.py | 29 ++++++++++---- jqmc/jqmc_mcmc.py | 76 +++++++++++++++++++++++-------------- tests/test_jqmc_gfmc_bra.py | 9 ++++- tests/test_jqmc_gfmc_tau.py | 9 ++++- tests/test_jqmc_mcmc.py | 25 +++++++++--- 6 files changed, 109 insertions(+), 46 deletions(-) diff --git a/jqmc/determinant.py b/jqmc/determinant.py index 11d6fd2f..22ea852d 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -2026,7 +2026,12 @@ def _grads_lap_bwd(res, g): G_bar_from_inv = -(G_inv_stable.T @ G_inv_bar @ G_inv_stable.T) # Step 3: propagate G_bar back through G = compute_geminal_all_elements(...). - _, vjp_fn2 = jax.vjp(compute_geminal_all_elements, geminal_data, r_up_carts, r_dn_carts) + # ``vjp_fn2`` is built around the un-cast call, whose primal output dtype is + # determined by (r_up_carts, r_dn_carts) (kinetic zone). In mixed precision the + # G_inv_bar / G_inv_stable arithmetic can promote ``G_bar_from_inv`` to a wider + # dtype, so cast back to the primal output dtype before invoking ``vjp_fn2``. + geminal_primal_out, vjp_fn2 = jax.vjp(compute_geminal_all_elements, geminal_data, r_up_carts, r_dn_carts) + G_bar_from_inv = jnp.asarray(G_bar_from_inv, dtype=geminal_primal_out.dtype) d_geminal_inv, d_r_up_inv, d_r_dn_inv = vjp_fn2(G_bar_from_inv) # Total: sum both contributions. diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index adefa34c..dcdda96b 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -57,7 +57,7 @@ from ._diff_mask import DiffMask, apply_diff_mask from ._jqmc_utility import _generate_init_electron_configurations -from ._precision import get_dtype +from ._precision import get_dtype, get_tolerance_min from ._setting import ( GFMC_MIN_BIN_BLOCKS, GFMC_MIN_COLLECT_STEPS, @@ -66,7 +66,6 @@ GFMC_ON_THE_FLY_COLLECT_STEPS, GFMC_ON_THE_FLY_WARMUP_STEPS, get_eps, - rtol_debug_vs_production, ) from .coulomb_potential import ( compute_bare_coulomb_potential_el_el, @@ -1051,7 +1050,10 @@ def _update_inv_up_t(_): Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv det_ratio = 1.0 + (v.T @ Ainv_u)[0, 0] - return A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio + # Cast back to A_old_inv.dtype: geminal elements live in the fp64 + # geminal zone, so the update would otherwise promote A_new_inv to + # fp64 and break the lax.cond dtype agreement with _no_update_t. + return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=A_old_inv.dtype) def _no_update_t(_): return A_old_inv @@ -1077,7 +1079,8 @@ def _update_inv_dn_t(_): Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv det_ratio = 1.0 + (v.T @ Ainv_u)[0, 0] - return A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio + # See note in _update_inv_up_t: cast back to A_old_inv.dtype. + return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=A_old_inv.dtype) if num_up_electrons == 0: A_new_inv = A_old_inv @@ -4759,7 +4762,10 @@ def _update_inv_up_n(_): Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv det_ratio = 1.0 + (v.T @ Ainv_u)[0, 0] - return A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio + # Cast back to A_old_inv.dtype: geminal elements live in the fp64 + # geminal zone, so the update would otherwise promote A_new_inv to + # fp64 and break the lax.cond dtype agreement with _no_update_n. + return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=A_old_inv.dtype) def _no_update_n(_): return A_old_inv @@ -4785,7 +4791,8 @@ def _update_inv_dn_n(_): Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv det_ratio = 1.0 + (v.T @ Ainv_u)[0, 0] - return A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio + # See note in _update_inv_up_n: cast back to A_old_inv.dtype. + return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=A_old_inv.dtype) if num_up_electrons == 0: A_new_inv = A_old_inv @@ -7350,12 +7357,18 @@ def _compute_local_energy_n_debug( for i in range(self.__num_walkers) ] ) - if np.max(np.abs(e_L_list - e_list_debug)) > rtol_debug_vs_production: + # e_L crosses orb_eval/jastrow/geminal/coulomb/kinetic/gfmc; bound + # the agreement by the weakest zone (fp32 in mixed precision). + _atol_eL, _rtol_eL = get_tolerance_min( + ("orb_eval", "jastrow", "geminal", "determinant", "coulomb", "kinetic", "gfmc"), + "strict", + ) + if np.max(np.abs(e_L_list - e_list_debug)) > _rtol_eL: logger.info(f"max(e_list - e_list_debug) = {np.max(np.abs(e_L_list - e_list_debug))}.") logger.info(f"w_L_list = {w_L_list}.") logger.info(f"e_L_list = {e_L_list}.") logger.info(f"e_list_debug = {e_list_debug}.") - np.testing.assert_almost_equal(np.array(e_L_list), np.array(e_list_debug), decimal=6) + np.testing.assert_allclose(np.array(e_L_list), np.array(e_list_debug), atol=_atol_eL, rtol=_rtol_eL) # atomic force related if self.__comput_position_deriv: diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index 53890a61..46b85f55 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -4406,40 +4406,56 @@ def body_fun(_, carry): )[0] # Determinant part, fast update using the matrix determinant lemma + # Cast both lax.cond branches to geminal_inv.dtype: the geminal-diff branch + # lives in the geminal zone (fp64) while jax.nn.one_hot defaults to fp32, + # so without an explicit cast the cond branches disagree in mixed precision. + _gem_dtype = geminal_inv.dtype v = lax.cond( is_up, - lambda _: ( - compute_geminal_up_one_row_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - # inline "as_row3": force (1,3) even if source is (3,) - r_up_cart=jnp.reshape(proposed_r_up_carts[selected_electron_index], (1, 3)), - r_dn_carts=r_dn_carts, - ) - - compute_geminal_up_one_row_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_cart=jnp.reshape(r_up_carts[selected_electron_index], (1, 3)), - r_dn_carts=r_dn_carts, - ) - )[:, None], - lambda _: jax.nn.one_hot(selected_electron_index, num_up_electrons)[:, None], + lambda _: jnp.asarray( + ( + compute_geminal_up_one_row_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + # inline "as_row3": force (1,3) even if source is (3,) + r_up_cart=jnp.reshape(proposed_r_up_carts[selected_electron_index], (1, 3)), + r_dn_carts=r_dn_carts, + ) + - compute_geminal_up_one_row_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_cart=jnp.reshape(r_up_carts[selected_electron_index], (1, 3)), + r_dn_carts=r_dn_carts, + ) + )[:, None], + dtype=_gem_dtype, + ), + lambda _: jnp.asarray( + jax.nn.one_hot(selected_electron_index, num_up_electrons)[:, None], + dtype=_gem_dtype, + ), operand=None, ) u = lax.cond( is_up, - lambda _: jax.nn.one_hot(selected_electron_index, num_up_electrons)[:, None], # (N_up, 1) - lambda _: ( - compute_geminal_dn_one_column_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_carts=r_up_carts, - r_dn_cart=jnp.reshape(proposed_r_dn_carts[selected_electron_index], (1, 3)), # inline "as_row3" - ) - - compute_geminal_dn_one_column_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_carts=r_up_carts, - r_dn_cart=jnp.reshape(r_dn_carts[selected_electron_index], (1, 3)), - ) - )[:, None], # -> (N_up, 1) + lambda _: jnp.asarray( + jax.nn.one_hot(selected_electron_index, num_up_electrons)[:, None], # (N_up, 1) + dtype=_gem_dtype, + ), + lambda _: jnp.asarray( + ( + compute_geminal_dn_one_column_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_carts=r_up_carts, + r_dn_cart=jnp.reshape(proposed_r_dn_carts[selected_electron_index], (1, 3)), # inline "as_row3" + ) + - compute_geminal_dn_one_column_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_carts=r_up_carts, + r_dn_cart=jnp.reshape(r_dn_carts[selected_electron_index], (1, 3)), + ) + )[:, None], # -> (N_up, 1) + dtype=_gem_dtype, + ), operand=None, ) @@ -4639,7 +4655,11 @@ def body_fun(_, carry): Det_T_ratio = 1.0 + (v.T @ Ainv_u)[0, 0] # scalar # (A+uv^T)^{-1} = A^{-1} - (A^{-1} u v^T A^{-1}) / (1 + v^T A^{-1} u) - geminal_inv_new = geminal_inv - (Ainv_u @ vT_Ainv) / Det_T_ratio + # Cast back to geminal_inv.dtype: ``v`` originates in the geminal zone (fp64) + # while geminal_inv lives in its own zone (e.g. fp32 in mixed precision), so + # the rank-1 update would otherwise promote and break the lax.cond dtype + # agreement with the rejected branch. + geminal_inv_new = jnp.asarray(geminal_inv - (Ainv_u @ vT_Ainv) / Det_T_ratio, dtype=geminal_inv.dtype) geminal_new = geminal.at[selected_electron_index, :].add(v.squeeze(-1)) diff --git a/tests/test_jqmc_gfmc_bra.py b/tests/test_jqmc_gfmc_bra.py index 121c511a..c3d6a7fc 100755 --- a/tests/test_jqmc_gfmc_bra.py +++ b/tests/test_jqmc_gfmc_bra.py @@ -45,7 +45,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -181,7 +181,12 @@ def test_jqmc_gfmc_n(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast ) gfmc_jax.run(num_mcmc_steps=num_mcmc_steps) - atol, rtol = get_tolerance("gfmc", "strict") + # e_L / w_L cross orb_eval/jastrow/geminal/coulomb/kinetic/gfmc zones; the + # achievable debug-vs-jax agreement is bounded by the weakest (fp32 in mixed). + atol, rtol = get_tolerance_min( + ("orb_eval", "jastrow", "geminal", "determinant", "coulomb", "kinetic", "gfmc"), + "strict", + ) if mpi_rank == 0: # w_L diff --git a/tests/test_jqmc_gfmc_tau.py b/tests/test_jqmc_gfmc_tau.py index eab8a6cb..ba7cacb1 100755 --- a/tests/test_jqmc_gfmc_tau.py +++ b/tests/test_jqmc_gfmc_tau.py @@ -45,7 +45,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -178,7 +178,12 @@ def test_jqmc_gfmc_t(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast ) gfmc_jax.run(num_mcmc_steps=num_mcmc_steps) - atol, rtol = get_tolerance("gfmc", "strict") + # e_L / e_L2 / w_L cross orb_eval/jastrow/geminal/coulomb/kinetic/gfmc zones; + # the achievable debug-vs-jax agreement is bounded by the weakest (fp32 in mixed). + atol, rtol = get_tolerance_min( + ("orb_eval", "jastrow", "geminal", "determinant", "coulomb", "kinetic", "gfmc"), + "strict", + ) if mpi_rank == 0: # w_L diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index d1e049be..bc61c4ec 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -47,7 +47,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 from jqmc.determinant import Geminal_data # noqa: E402 from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 @@ -80,7 +80,12 @@ @pytest.mark.parametrize("trexio_file,with_1b_jastrow,with_2b_jastrow,with_3b_jastrow,with_nn_jastrow", param_grid) def test_jqmc_mcmc(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jastrow, with_nn_jastrow): """Test comparison with MCMC debug and MCMC production implementations.""" - atol, rtol = get_tolerance("mcmc", "strict") + # e_L / w_L cross orb_eval/jastrow/geminal/coulomb/kinetic/mcmc zones; the + # achievable debug-vs-jax agreement is bounded by the weakest (fp32 in mixed). + atol, rtol = get_tolerance_min( + ("orb_eval", "jastrow", "geminal", "determinant", "coulomb", "kinetic", "mcmc"), + "strict", + ) ( structure_data, _, @@ -1029,7 +1034,11 @@ def fake_get_gF( ln_psi_mo = float(evaluate_ln_wavefunction(final_wf, r_up, r_dn)) ln_psi_ao = float(evaluate_ln_wavefunction(wf_ao, r_up, r_dn)) - atol, rtol = get_tolerance("mcmc", "strict") + # ln|Psi| crosses orb_eval/jastrow/geminal/determinant; bound by weakest zone. + atol, rtol = get_tolerance_min( + ("orb_eval", "jastrow", "geminal", "determinant", "mcmc"), + "strict", + ) assert not np.any(np.isnan(np.asarray(ln_psi_mo))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(ln_psi_ao))), "NaN detected in second argument" np.testing.assert_allclose(ln_psi_mo, ln_psi_ao, atol=atol, rtol=rtol) @@ -1211,7 +1220,9 @@ def fake_get_gF( lam_after = np.asarray(mcmc.hamiltonian_data.wavefunction_data.geminal_data.lambda_matrix) # ── Assertions ─────────────────────────────────────────────────────────── - atol, rtol = get_tolerance("mcmc", "strict") + # j3 / lambda_matrix live in jastrow / geminal zones; symmetry is a structural + # property of the matrix itself, so use those zones' tolerances. + atol, rtol = get_tolerance_min(("jastrow", "geminal"), "strict") if j3_type == "sym": np.testing.assert_allclose( j3_after[:, :-1], @@ -1598,7 +1609,11 @@ def test_get_aH_and_solve_lm_debug_vs_production(): # Get variational blocks blocks = hamiltonian_data.wavefunction_data.get_variational_blocks() - atol, rtol = get_tolerance("mcmc", "strict") + # H_0/f/S/K/B cross the full e_L path + optimization assembly; bound by weakest zone. + atol, rtol = get_tolerance_min( + ("orb_eval", "jastrow", "geminal", "determinant", "coulomb", "kinetic", "mcmc", "optimization"), + "strict", + ) # --- Test 1: get_aH in LM mode (return_matrices=True) --- H_0_d, f_d, S_d, K_d, B_d = mcmc_debug.get_aH( From aa3e64ce7a513498ffc8fb2e6f6a4ea8b3863e12 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sat, 25 Apr 2026 00:14:31 +0900 Subject: [PATCH 05/97] fix(precision): cast r-R differences to float64 before downcast to avoid catastrophic cancellation When mixed-precision zones (orb_eval, jastrow, coulomb) are set to float32, relative coordinate differences r - R (and r_i - r_j) suffer catastrophic cancellation for systems with large absolute coordinates (e.g. R ~ 50 Bohr loses ~6 digits of precision in float32). This propagated through exp(-Z*r^2), (x+eps)^nx, and 1/|r-R|, producing energy errors of several hartree (e.g. -134 vs -137 Ha) far beyond expected float32 round-off. --- jqmc/_setting.py | 6 ++-- jqmc/atomic_orbital.py | 72 +++++++++++++++++++++++++-------------- jqmc/coulomb_potential.py | 16 ++++++--- jqmc/jastrow_factor.py | 21 +++++++++--- jqmc/structure.py | 8 +++-- 5 files changed, 83 insertions(+), 40 deletions(-) diff --git a/jqmc/_setting.py b/jqmc/_setting.py index bdf2b60a..58385216 100644 --- a/jqmc/_setting.py +++ b/jqmc/_setting.py @@ -102,9 +102,9 @@ # stabilizing_ao — small epsilon for AO Cartesian derivative stabilization. # rcond_svd — threshold for SVD pseudoinverse of the geminal matrix. _EPS_DTYPE_AWARE: dict[str, dict[str, float]] = { - "machine_precision": {"float64": 1e-300, "float32": 1e-38}, - "stabilizing_ao": {"float64": 1e-16, "float32": 1e-7}, - "rcond_svd": {"float64": 1e-20, "float32": 1e-6}, + "machine_precision": {"float64": 1e-38, "float32": 1e-38}, + "stabilizing_ao": {"float64": 1e-16, "float32": 1e-12}, + "rcond_svd": {"float64": 1e-20, "float32": 1e-16}, } diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index 29106081..62b5af70 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -1988,8 +1988,13 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A """ # Downcast all float inputs to orb_eval zone dtype (P0-1, P0-2) dtype = get_dtype("orb_eval") - r_carts = r_carts.astype(dtype) - R_carts_jnp = aos_data._atomic_center_carts_prim_jnp.astype(dtype) + # Compute r-R in float64 to avoid catastrophic cancellation when zone dtype is float32 + # (positions can be ~50 Bohr; float32 difference loses ~6 digits of precision). + _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) + _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) + r_R_diffs = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) + r_carts = _r_carts_f64.astype(dtype) + R_carts_jnp = _R_carts_f64.astype(dtype) c_jnp = aos_data._coefficients_jnp.astype(dtype) Z_jnp = aos_data._exponents_jnp.astype(dtype) l_jnp = aos_data._angular_momentums_prim_jnp @@ -2000,7 +2005,6 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A N_n_dup_fuctorial_part = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype) N_n_dup_Z_part = (2.0 * Z_jnp / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z_jnp) ** l_jnp N_n_dup = jnp.sqrt(N_n_dup_Z_part * N_n_dup_fuctorial_part) - r_R_diffs = r_carts[None, :, :] - R_carts_jnp[:, None, :] r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) @@ -2037,16 +2041,22 @@ def _compute_AOs_sphe(aos_data: AOs_sphe_data, r_carts: jnpt.ArrayLike) -> jax.A """ # Downcast all float inputs to orb_eval zone dtype (P0-1) dtype = get_dtype("orb_eval") - r_carts = r_carts.astype(dtype) + # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. + _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) + _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) + _R_carts_unique_f64 = jnp.asarray(aos_data._atomic_center_carts_unique_jnp, dtype=jnp.float64) + r_R_diffs = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) + r_R_diffs_uq = (_r_carts_f64[None, :, :] - _R_carts_unique_f64[:, None, :]).astype(dtype) + r_carts = _r_carts_f64.astype(dtype) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp - R_carts_jnp = aos_data._atomic_center_carts_prim_jnp.astype(dtype) - R_carts_unique_jnp = aos_data._atomic_center_carts_unique_jnp.astype(dtype) + R_carts_jnp = _R_carts_f64.astype(dtype) + R_carts_unique_jnp = _R_carts_unique_f64.astype(dtype) c_jnp = aos_data._coefficients_jnp.astype(dtype) Z_jnp = aos_data._exponents_jnp.astype(dtype) l_jnp = aos_data._angular_momentums_prim_jnp m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp - # Normalization constants computed in zone dtype (P0-1: replaces .astype(jnp.float64)) + # Normalization constants computed in zone dtype. l_typed = l_jnp.astype(dtype) factorial_l_plus_1 = jnp.exp(jscipy.special.gammaln(l_typed + 2.0)) factorial_2l_plus_2 = jnp.exp(jscipy.special.gammaln(2.0 * l_typed + 3.0)) @@ -2056,10 +2066,8 @@ def _compute_AOs_sphe(aos_data: AOs_sphe_data, r_carts: jnpt.ArrayLike) -> jax.A / (factorial_2l_plus_2 * jnp.sqrt(jnp.asarray(jnp.pi, dtype=dtype))) ) N_l_m_dup = jnp.sqrt((2 * l_typed + 1) / (4 * jnp.asarray(jnp.pi, dtype=dtype))) - r_R_diffs = r_carts[None, :, :] - R_carts_jnp[:, None, :] r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) - r_R_diffs_uq = r_carts[None, :, :] - R_carts_unique_jnp[:, None, :] max_ml, S_l_m_dup_all_l_m = _compute_S_l_m(r_R_diffs_uq) S_l_m_dup_all_l_m_reshaped = S_l_m_dup_all_l_m.reshape( @@ -2568,8 +2576,12 @@ def _single_val_grad_lap(diff: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.A def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for Cartesian AOs (contracted).""" dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts, dtype=dtype) - R_carts = aos_data._atomic_center_carts_prim_jnp.astype(dtype) + # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. + _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) + _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) + diff = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) + r_carts = _r_carts_f64.astype(dtype) + R_carts = _R_carts_f64.astype(dtype) c = aos_data._coefficients_jnp.astype(dtype) Z = aos_data._exponents_jnp.astype(dtype) l = aos_data._angular_momentums_prim_jnp @@ -2581,7 +2593,6 @@ def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.n N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z) ** l N = jnp.sqrt(N_Z * N_fact) - diff = r_carts[None, :, :] - R_carts[:, None, :] x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] eps = get_eps("stabilizing_ao", dtype) x = x + eps @@ -2614,10 +2625,16 @@ def _second_component(base, n): def _compute_AOs_laplacian_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for spherical AOs (contracted).""" dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts, dtype=dtype) + # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. + _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) + _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) + _R_carts_unique_f64 = jnp.asarray(aos_data._atomic_center_carts_unique_jnp, dtype=jnp.float64) + r_R_diffs = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) + r_R_diffs_uq = (_r_carts_f64[None, :, :] - _R_carts_unique_f64[:, None, :]).astype(dtype) + r_carts = _r_carts_f64.astype(dtype) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp - R_carts_jnp = aos_data._atomic_center_carts_prim_jnp.astype(dtype) - R_carts_unique_jnp = aos_data._atomic_center_carts_unique_jnp.astype(dtype) + R_carts_jnp = _R_carts_f64.astype(dtype) + R_carts_unique_jnp = _R_carts_unique_f64.astype(dtype) c_jnp = aos_data._coefficients_jnp.astype(dtype) Z_jnp = aos_data._exponents_jnp.astype(dtype) l_jnp = aos_data._angular_momentums_prim_jnp @@ -2633,11 +2650,9 @@ def _compute_AOs_laplacian_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.n ) N_l_m_dup = jnp.sqrt((2 * l_f64 + 1) / (4 * jnp.pi)) - r_R_diffs = r_carts[None, :, :] - R_carts_jnp[:, None, :] r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) - r_R_diffs_uq = r_carts[None, :, :] - R_carts_unique_jnp[:, None, :] S_l_m_vals_all, S_l_m_grads_all, S_l_m_laps_all = _compute_S_l_m_and_grad_lap(r_R_diffs_uq) max_ml = S_l_m_vals_all.shape[0] @@ -2876,8 +2891,12 @@ def _compute_AOs_laplacian_debug( def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for Cartesian AOs (contracted).""" dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts, dtype=dtype) - R_carts = aos_data._atomic_center_carts_prim_jnp.astype(dtype) + # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. + _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) + _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) + diff = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) + r_carts = _r_carts_f64.astype(dtype) + R_carts = _R_carts_f64.astype(dtype) c = aos_data._coefficients_jnp.astype(dtype) Z = aos_data._exponents_jnp.astype(dtype) l = aos_data._angular_momentums_prim_jnp @@ -2889,7 +2908,6 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z) ** l N = jnp.sqrt(N_Z * N_fact) - diff = r_carts[None, :, :] - R_carts[:, None, :] x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] eps = get_eps("stabilizing_ao", dtype) x = x + eps @@ -2925,10 +2943,16 @@ def _grad_component(base, n): def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for spherical AOs (contracted).""" dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts, dtype=dtype) + # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. + _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) + _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) + _R_carts_unique_f64 = jnp.asarray(aos_data._atomic_center_carts_unique_jnp, dtype=jnp.float64) + r_R_diffs = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) + r_R_diffs_uq = (_r_carts_f64[None, :, :] - _R_carts_unique_f64[:, None, :]).astype(dtype) + r_carts = _r_carts_f64.astype(dtype) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp - R_carts_jnp = aos_data._atomic_center_carts_prim_jnp.astype(dtype) - R_carts_unique_jnp = aos_data._atomic_center_carts_unique_jnp.astype(dtype) + R_carts_jnp = _R_carts_f64.astype(dtype) + R_carts_unique_jnp = _R_carts_unique_f64.astype(dtype) c_jnp = aos_data._coefficients_jnp.astype(dtype) Z_jnp = aos_data._exponents_jnp.astype(dtype) l_jnp = aos_data._angular_momentums_prim_jnp @@ -2944,11 +2968,9 @@ def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarra ) N_l_m_dup = jnp.sqrt((2 * l_f64 + 1) / (4 * jnp.pi)) - r_R_diffs = r_carts[None, :, :] - R_carts_jnp[:, None, :] r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) - r_R_diffs_uq = r_carts[None, :, :] - R_carts_unique_jnp[:, None, :] max_ml, S_l_m_dup_all_l_m = _compute_S_l_m(r_R_diffs_uq) _, S_l_m_grad_all_l_m, _ = _compute_S_l_m_and_grad_lap(r_R_diffs_uq) diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 8300ee40..1968935f 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -2263,7 +2263,9 @@ def compute_bare_coulomb_potential_el_ion_element_wise( # Define a function to compute interaction for a pair def el_ion_interaction(Z_i, Z_j, r_i, r_j): - distance = jnp.linalg.norm(r_i - r_j, axis=1) + # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. + diff = (r_i.astype(jnp.float64) - r_j.astype(jnp.float64)).astype(r_i.dtype) + distance = jnp.linalg.norm(diff, axis=1) interaction = (Z_i * Z_j) / distance return interaction @@ -2308,7 +2310,9 @@ def compute_discretized_bare_coulomb_potential_el_ion_element_wise( # Define a function to compute interaction for a pair def el_ion_interaction(Z_i, Z_j, r_i, r_j, alat): - distance = jnp.maximum(jnp.linalg.norm(r_i - r_j, axis=1), alat) + # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. + diff = (r_i.astype(jnp.float64) - r_j.astype(jnp.float64)).astype(r_i.dtype) + distance = jnp.maximum(jnp.linalg.norm(diff, axis=1), alat) interaction = (Z_i * Z_j) / distance return interaction @@ -2443,7 +2447,9 @@ def compute_bare_coulomb_potential_el_el( # Define a function to compute interaction for a pair def el_el_interaction(Z_i, Z_j, r_i, r_j): - distance = jnp.linalg.norm(r_i - r_j) + # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. + diff = (r_i.astype(jnp.float64) - r_j.astype(jnp.float64)).astype(r_i.dtype) + distance = jnp.linalg.norm(diff) interaction = (Z_i * Z_j) / distance return interaction @@ -2492,7 +2498,9 @@ def compute_bare_coulomb_potential_ion_ion( # Define a function to compute interaction for a pair def ion_ion_interaction(Z_i, Z_j, r_i, r_j): - distance = jnp.linalg.norm(r_i - r_j) + # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. + diff = (r_i.astype(jnp.float64) - r_j.astype(jnp.float64)).astype(r_i.dtype) + distance = jnp.linalg.norm(diff) interaction = (Z_i * Z_j) / distance return interaction diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 91ae2a2d..76a29681 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -436,7 +436,9 @@ def _pairwise_distances(self, A: jnp.ndarray, B: jnp.ndarray) -> jnp.ndarray: dtype_j = get_dtype("jastrow") if A.shape[0] == 0 or B.shape[0] == 0: return jnp.zeros((A.shape[0], B.shape[0]), dtype=dtype_j) - diff = A[:, None, :] - B[None, :, :] + # Compute differences in float64 to avoid catastrophic cancellation + # under float32 jastrow zone, then downcast. + diff = (A.astype(jnp.float64)[:, None, :] - B.astype(jnp.float64)[None, :, :]).astype(dtype_j) return jnp.sqrt(jnp.sum(diff**2, axis=-1) + EPS_safe_distance) def _nuclear_embeddings(self, Z_n: jnp.ndarray) -> jnp.ndarray: @@ -676,7 +678,9 @@ def one_body_jastrow_kernel( R_cart: jnpt.ArrayLike, ) -> float: """Exponential form of J1.""" - return 1.0 / (2.0 * param) * (1.0 - jnp.exp(-param * coeff * jnp.linalg.norm(r_cart - R_cart))) + # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. + diff = (r_cart.astype(jnp.float64) - R_cart.astype(jnp.float64)).astype(r_cart.dtype) + return 1.0 / (2.0 * param) * (1.0 - jnp.exp(-param * coeff * jnp.linalg.norm(diff))) def atom_contrib(r_cart, R_cart, Z_eff): coeff = (2.0 * Z_eff) ** (1.0 / 4.0) @@ -686,7 +690,9 @@ def atom_contrib(r_cart, R_cart, Z_eff): def atom_contrib(r_cart, R_cart, Z_eff): """Pade form of J1: -Z_eff^{3/4} * r_eN / (2*(1 + a * Z_eff^{1/4} * r_eN)).""" - r_eN = jnp.linalg.norm(r_cart - R_cart) + # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. + diff = (r_cart.astype(jnp.float64) - R_cart.astype(jnp.float64)).astype(r_cart.dtype) + r_eN = jnp.linalg.norm(diff) coeff = (2.0 * Z_eff) ** (1.0 / 4.0) return -((2.0 * Z_eff) ** (3.0 / 4.0)) * r_eN / (2.0 * (1.0 + j1b * coeff * r_eN)) @@ -1114,11 +1120,16 @@ def compute_Jastrow_two_body( def two_body_jastrow_exp(param: float, r_cart_i: jnpt.ArrayLike, r_cart_j: jnpt.ArrayLike) -> float: """Exponential form of J2.""" - return 1.0 / (2.0 * param) * (1.0 - jnp.exp(-param * jnp.linalg.norm(r_cart_i - r_cart_j))) + # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. + diff = (r_cart_i.astype(jnp.float64) - r_cart_j.astype(jnp.float64)).astype(r_cart_i.dtype) + return 1.0 / (2.0 * param) * (1.0 - jnp.exp(-param * jnp.linalg.norm(diff))) def two_body_jastrow_pade(param: float, r_cart_i: jnpt.ArrayLike, r_cart_j: jnpt.ArrayLike) -> float: """Pade form of J2.""" - return jnp.linalg.norm(r_cart_i - r_cart_j) / 2.0 * (1.0 + param * jnp.linalg.norm(r_cart_i - r_cart_j)) ** (-1.0) + # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. + diff = (r_cart_i.astype(jnp.float64) - r_cart_j.astype(jnp.float64)).astype(r_cart_i.dtype) + r_ij = jnp.linalg.norm(diff) + return r_ij / 2.0 * (1.0 + param * r_ij) ** (-1.0) if j2b_type == "pade": two_body_jastrow_anti_parallel = two_body_jastrow_pade diff --git a/jqmc/structure.py b/jqmc/structure.py index ce31f73f..0dad1c8b 100755 --- a/jqmc/structure.py +++ b/jqmc/structure.py @@ -466,9 +466,11 @@ def _get_min_dist_rel_R_cart_jnp( """ if dtype is None: dtype = get_dtype("io") - r_cart = jnp.array(r_cart, dtype=dtype) - R_cart = jnp.array(structure_data._positions_cart_jnp[i_atom], dtype=dtype) - diff = R_cart - r_cart + # Compute R - r in float64 to avoid catastrophic cancellation under float32 zones, + # then downcast. + _r_f64 = jnp.asarray(r_cart, dtype=jnp.float64) + _R_f64 = jnp.asarray(structure_data._positions_cart_jnp[i_atom], dtype=jnp.float64) + diff = (_R_f64 - _r_f64).astype(dtype) if structure_data.pbc_flag: cell = jnp.array(structure_data.cell, dtype=dtype) inv_cell = jnp.linalg.inv(cell) From 4ed37f18794ca9a44d3a33b4916d487c800a61ff Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sat, 25 Apr 2026 01:06:23 +0900 Subject: [PATCH 06/97] fix(precision): keep AGP/SD geminal in fp64 to prevent fp32 amplification of log|det| Diagnostic scripts in bug/fp32/ showed that with the previous mixed-precision defaults (orb_eval=fp32, geminal=fp32), local energies on a 32-up/32-dn ECP system were biased vs full fp64. The bias came from the kinetic term: AO matrix entries had only fp32 round-off, but the determinant amplified that into a log|det| error, which fed back into both T and ECP non-local matrix elements. Two minimal changes restore safety while keeping the heavy AO kernels in fp32: 1. molecular_orbital.compute_MOs now upcasts the (small) MO matmul to the determinant-zone dtype (fp64 in mixed mode). The expensive AO evaluation still runs in orb_eval (fp32), but the MO matrix returned to the determinant / kinetic / energy paths is fp64, breaking the chain "fp32 AO -> fp32 geminal -> noisy log|det|". 2. _precision._DEFAULTS_MIXED["geminal"] is changed from float32 to float64. The geminal matrix is the input to LU/det; even ~1e-7 entry noise blows up to O(1) errors in log|det|. The diag_04 AGP sweep additionally shows that AO-direct (AGP) form is ~4x more sensitive to geminal=fp32 than the MO (JSD) form so this default is required for both ansatz types. --- jqmc/_precision.py | 13 +++++++++---- jqmc/molecular_orbital.py | 15 +++++++++------ tests/test_mixed_precision.py | 19 ++++++++++++------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/jqmc/_precision.py b/jqmc/_precision.py index c4b942d0..cfb654b3 100644 --- a/jqmc/_precision.py +++ b/jqmc/_precision.py @@ -111,9 +111,14 @@ def compute_AOs(aos_data, r_carts): # --- mode="mixed" defaults (recommended mixed precision) --- # float32 risk: -# orb_eval - low: smooth Gaussian basis + linear combination +# orb_eval - low: smooth Gaussian basis + linear combination. +# (Heavy AO eval stays in fp32; compute_MOs upcasts the small +# matmul to the determinant zone, see molecular_orbital.py.) # jastrow - low: smooth correlation function, pre-exp value -# geminal - low: building matrix elements only (pre-det) +# geminal - HIGH: this matrix is the input to LU/det; even ε≈1e-7 on +# entries amplifies into log|det| errors of O(1) for ~32x32 +# systems with non-trivial condition numbers +# (see bug/fp32 diagnostics). Kept in fp64. # coulomb - low-medium: sum of 1/r + ECP spherical quadrature # determinant - high: log(det) cancellation, SVD 1/s, eigenvalue ops # kinetic - high: second derivative of ln|Psi|, cancellation-sensitive @@ -122,9 +127,9 @@ def compute_AOs(aos_data, r_carts): # optimization- high: S^{-1}F linear system, ill-conditioned matrix # io - low-medium: file I/O + nuclear coordinates _DEFAULTS_MIXED: dict[str, str] = { - "orb_eval": "float32", # low risk + "orb_eval": "float32", # low risk (heavy kernel only) "jastrow": "float32", # low risk - "geminal": "float32", # low risk + "geminal": "float64", # high risk: feeds LU/det "determinant": "float64", # high risk "coulomb": "float32", # low-medium risk "kinetic": "float64", # high risk diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index c9fc822d..aa074590 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -232,12 +232,15 @@ def compute_MOs(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: Returns: jax.Array: MO values with shape ``(num_mo, N_e)``. """ - dtype = get_dtype("orb_eval") - mo_coefficients = mos_data.mo_coefficients.astype(dtype) - answer = jnp.dot( - mo_coefficients, - compute_AOs(aos_data=mos_data.aos_data, r_carts=r_carts), - ) + # Heavy AO evaluation runs in ``orb_eval`` precision (e.g. fp32 in mixed mode), + # but the (small) MO matmul and the returned MO matrix are kept in the + # ``determinant`` precision (fp64 by default). This avoids amplifying fp32 + # round-off through downstream determinant / kinetic / energy paths while + # preserving the speed of the AO kernels (see bug/fp32 diagnostics). + out_dtype = get_dtype("determinant") + aos = compute_AOs(aos_data=mos_data.aos_data, r_carts=r_carts).astype(out_dtype) + mo_coefficients = mos_data.mo_coefficients.astype(out_dtype) + answer = jnp.dot(mo_coefficients, aos) return answer diff --git a/tests/test_mixed_precision.py b/tests/test_mixed_precision.py index 92027e48..8fc992fd 100644 --- a/tests/test_mixed_precision.py +++ b/tests/test_mixed_precision.py @@ -206,18 +206,23 @@ def test_compute_AOs_laplacian_output_dtype(self, h2_data): class TestMODtype: - """Verify MO evaluation outputs are float32 in mixed mode.""" + """Verify MO evaluation outputs use the determinant-zone dtype. - def test_compute_MOs_output_dtype(self, h2_data): - """compute_MOs must return float32 (orb_eval zone). + Note: AO evaluation itself runs in ``orb_eval`` precision (e.g. fp32 in mixed + mode), but the (small) MO matmul is upcast to the ``determinant`` zone dtype + (fp64 by default). This avoids amplifying fp32 round-off through the + 32x32 determinant / kinetic / energy paths while keeping the heavy AO + kernels in fp32 (see bug/fp32 diagnostics). + """ - Catches: mo_coefficients (fp64) × AOs (fp32) promotion. - """ + def test_compute_MOs_output_dtype(self, h2_data): + """compute_MOs must return determinant-zone dtype (fp64 in mixed).""" MOs = compute_MOs(h2_data["mos_data_up"], h2_data["r_up"]) - expected = get_dtype("orb_eval") + expected = get_dtype("determinant") assert MOs.dtype == expected, ( f"compute_MOs output dtype is {MOs.dtype}, expected {expected}. " - "Likely cause: mo_coefficients not cast to orb_eval dtype." + "compute_MOs should upcast its small matmul to the determinant zone " + "to avoid fp32 amplification downstream." ) From 92eac7af4081636db81f9f24c80c436f75d9bb78 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:34:33 +0900 Subject: [PATCH 07/97] refactor(precision): simplify API to mode-only ("full"/"mixed") Users now choose only `"full"` or `"mixed"`; per-zone overrides have been removed from TOML, workflow parameters, and `configure()`. Zone assignments now live in `_FULL_PRECISION` and `_MIXED_PRECISION` dictionaries, renamed from `_DEFAULTS_*`. Developers who need per-zone control can edit those dictionaries directly or use `_set_zone()`. --- doc/notes/mixed_precision.md | 38 +++-------- examples/jqmc-example01/02vmc_JSD/vmc.toml | 11 ---- jqmc/_precision.py | 73 +++++++++++----------- jqmc/jqmc_cli.py | 15 ++++- jqmc_workflow/lrdmc_workflow.py | 11 +--- jqmc_workflow/mcmc_workflow.py | 11 +--- jqmc_workflow/vmc_workflow.py | 11 +--- tests/conftest.py | 2 +- tests/test_mixed_precision.py | 4 +- 9 files changed, 67 insertions(+), 109 deletions(-) diff --git a/doc/notes/mixed_precision.md b/doc/notes/mixed_precision.md index c81116f7..f10aee73 100644 --- a/doc/notes/mixed_precision.md +++ b/doc/notes/mixed_precision.md @@ -30,14 +30,14 @@ Or keep the default (all float64, backward compatible): ## Precision zones -jQMC divides the computation into 10 **Precision Zones**, each independently -configurable: +jQMC divides the computation into 10 **Precision Zones**. The mapping +from zone to dtype is determined entirely by the chosen mode: | Zone | Components | `full` | `mixed` | float32 risk | |----------------|-----------------------------------|--------|---------|--------------| | `orb_eval` | AO/MO forward evaluation | f64 | **f32** | low | | `jastrow` | Jastrow factor (J1/J2/J3) | f64 | **f32** | low | -| `geminal` | Geminal matrix elements | f64 | **f32** | low | +| `geminal` | Geminal matrix elements | f64 | f64 | high | | `determinant` | log-det, SVD, AS regularization | f64 | f64 | high | | `coulomb` | Coulomb + ECP potential | f64 | **f32** | low-medium | | `kinetic` | Kinetic energy + AO/MO derivatives | f64 | f64 | high | @@ -46,25 +46,9 @@ configurable: | `optimization` | SR matrix, parameter updates | f64 | f64 | high | | `io` | I/O, structure data | f64 | f64 | low-medium | -## Custom zone configuration - -Individual zones can be overridden regardless of the base mode: - -```toml -[precision] -mode = "mixed" # start from recommended mixed defaults -orb_eval = "float64" # override: keep AO/MO in float64 -``` - -```toml -[precision] -mode = "full" # start from all float64 -orb_eval = "float32" # override: only AO/MO in float32 -``` - ## Workflow integration -When using `jqmc_workflow`, pass precision settings to any workflow class: +When using `jqmc_workflow`, pass the precision mode to any workflow class: ```python from jqmc_workflow import VMC_Workflow @@ -76,16 +60,10 @@ wf = VMC_Workflow( ) ``` -For custom per-zone overrides: - -```python -wf = VMC_Workflow( - server_machine_name="cluster", - num_opt_steps=20, - precision_mode="mixed", - precision_overrides={"orb_eval": "float64"}, -) -``` +Per-zone assignments are defined in `_FULL_PRECISION` / `_MIXED_PRECISION` +inside `jqmc/_precision.py` and are not configurable from TOML or workflow +parameters. Developers who need per-zone control for diagnostics can edit +those dicts directly or use `_set_zone()` after calling `configure()`. ## Design principles diff --git a/examples/jqmc-example01/02vmc_JSD/vmc.toml b/examples/jqmc-example01/02vmc_JSD/vmc.toml index 1bcad5fd..cf455e76 100644 --- a/examples/jqmc-example01/02vmc_JSD/vmc.toml +++ b/examples/jqmc-example01/02vmc_JSD/vmc.toml @@ -31,14 +31,3 @@ opt_lambda_basis_coeff = false # [precision] # mode = "full" # "full" (default, all float64) or "mixed" (recommended mixed precision) -# # Per-zone overrides (optional, override the mode defaults): -# # orb_eval = "float32" # AO/MO forward evaluation -# # jastrow = "float32" # Jastrow factor -# # geminal = "float32" # Geminal matrix elements -# # determinant = "float64" # log-det, SVD, AS regularization -# # coulomb = "float32" # Coulomb + ECP potential -# # kinetic = "float64" # Kinetic energy + derivatives -# # mcmc = "float64" # MCMC sampling -# # gfmc = "float64" # GFMC propagation -# # optimization = "float64" # SR matrix, parameter updates -# # io = "float64" # I/O, structure data diff --git a/jqmc/_precision.py b/jqmc/_precision.py index cfb654b3..fa5a83a2 100644 --- a/jqmc/_precision.py +++ b/jqmc/_precision.py @@ -5,12 +5,17 @@ does NOT rely on JAX's implicit dtype propagation, ensuring robustness against future changes in JAX's type promotion semantics. -All zones are user-configurable. Defaults depend on the mode: +Users choose one of two modes: -- ``mode="full"`` (default) — all zones float64 (backward compatible). -- ``mode="mixed"`` — recommended mixed precision; low-risk zones become +- ``"full"`` (default) — all zones float64 (backward compatible). +- ``"mixed"`` — recommended mixed precision; low-risk zones become float32 while numerically sensitive zones stay float64. +Individual zone assignments are **not** user-configurable; they are +defined in ``_FULL_PRECISION`` and ``_MIXED_PRECISION`` below. +Developers who need to tweak per-zone dtypes should edit those dicts +directly in this file. + Precision Zones --------------- @@ -19,7 +24,7 @@ ============== ============================ ========= ======== ============ ``orb_eval`` AO/MO forward evaluation float64 float32 low ``jastrow`` Jastrow factor (J1/J2/J3) float64 float32 low -``geminal`` Geminal matrix elements float64 float32 low +``geminal`` Geminal matrix elements float64 float64 high ``determinant`` log-det, SVD, AS reg. float64 float64 high ``coulomb`` Coulomb + ECP potential float64 float32 low-medium ``kinetic`` Kinetic energy + AO/MO derivs float64 float64 high @@ -95,8 +100,8 @@ def compute_AOs(aos_data, r_carts): logger = logging.getLogger(__name__) -# --- mode="full" defaults (all float64, backward compatible) --- -_DEFAULTS_FULL: dict[str, str] = { +# --- mode="full" (all float64, backward compatible) --- +_FULL_PRECISION: dict[str, str] = { "orb_eval": "float64", # AO/MO forward evaluation "jastrow": "float64", # Jastrow factor "geminal": "float64", # Geminal matrix elements @@ -109,7 +114,7 @@ def compute_AOs(aos_data, r_carts): "io": "float64", # I/O, structure data } -# --- mode="mixed" defaults (recommended mixed precision) --- +# --- mode="mixed" (recommended mixed precision) --- # float32 risk: # orb_eval - low: smooth Gaussian basis + linear combination. # (Heavy AO eval stays in fp32; compute_MOs upcasts the small @@ -126,7 +131,7 @@ def compute_AOs(aos_data, r_carts): # gfmc - high: weighted branching/pruning, population collapse in float32 # optimization- high: S^{-1}F linear system, ill-conditioned matrix # io - low-medium: file I/O + nuclear coordinates -_DEFAULTS_MIXED: dict[str, str] = { +_MIXED_PRECISION: dict[str, str] = { "orb_eval": "float32", # low risk (heavy kernel only) "jastrow": "float32", # low risk "geminal": "float64", # high risk: feeds LU/det @@ -139,7 +144,7 @@ def compute_AOs(aos_data, r_carts): "io": "float64", # low-medium risk } -ALL_ZONES = frozenset(_DEFAULTS_FULL.keys()) +ALL_ZONES = frozenset(_FULL_PRECISION.keys()) # Runtime zone -> dtype mapping _zone_dtypes: dict[str, type] = {} @@ -165,50 +170,48 @@ def _str_to_dtype(s: str) -> type: raise ValueError(f"Invalid dtype '{s}'. Must be 'float32' or 'float64'.") -def configure(precision_config: dict[str, str]) -> None: - """Set zone-level dtypes from a TOML ``[precision]`` section. +def configure(mode: str = "full") -> None: + """Activate a precision mode. Args: - precision_config: Mapping such as ``{"mode": "mixed", "orb_eval": "float32", ...}``. - - - ``mode="full"`` (default): all zones float64. - - ``mode="mixed"``: recommended mixed precision defaults. - - ``mode`` omitted: same as ``"full"``. - - Per-zone overrides take highest priority regardless of *mode*. + mode: ``"full"`` (default, all float64) or ``"mixed"`` + (recommended mixed precision). Raises: - ValueError: If *mode* is unknown, a zone name is invalid, or a dtype - string is not ``"float32"``/``"float64"``. + ValueError: If *mode* is not ``"full"`` or ``"mixed"``. """ _zone_dtypes.clear() - mode = precision_config.get("mode", "full") - - # Select base configuration if mode == "full": - base = dict(_DEFAULTS_FULL) + base = _FULL_PRECISION elif mode == "mixed": - base = dict(_DEFAULTS_MIXED) + base = _MIXED_PRECISION else: raise ValueError(f"Unknown precision mode '{mode}'. Must be 'full' or 'mixed'.") - # Apply per-zone overrides - for zone, dtype_str in precision_config.items(): - if zone == "mode": - continue - if zone not in ALL_ZONES: - raise ValueError(f"Unknown precision zone '{zone}'. Available zones: {sorted(ALL_ZONES)}") - _str_to_dtype(dtype_str) # validate - base[zone] = dtype_str - - # Build final mapping for zone, dtype_str in base.items(): _zone_dtypes[zone] = _str_to_dtype(dtype_str) - # Log the precision configuration logger.info(summary()) +def _set_zone(zone: str, dtype_str: str) -> None: + """Override a single zone's dtype at runtime (developer use only). + + Must be called **after** :func:`configure`. This is intentionally + private — normal users select ``"full"`` or ``"mixed"`` mode and the + per-zone mapping is determined by ``_FULL_PRECISION`` / + ``_MIXED_PRECISION``. + + Args: + zone: Precision Zone name. + dtype_str: ``"float32"`` or ``"float64"``. + """ + if zone not in ALL_ZONES: + raise ValueError(f"Unknown precision zone '{zone}'. Available zones: {sorted(ALL_ZONES)}") + _zone_dtypes[zone] = _str_to_dtype(dtype_str) + + def get_dtype(zone: str) -> type: """Return the dtype for a Precision Zone. diff --git a/jqmc/jqmc_cli.py b/jqmc/jqmc_cli.py index c790b506..f204fc9d 100644 --- a/jqmc/jqmc_cli.py +++ b/jqmc/jqmc_cli.py @@ -244,10 +244,19 @@ def _cli(): logger.info("") # --- precision configuration --- - precision_config = dict_toml.get("precision", {}) - if not isinstance(precision_config, dict): + precision_section = dict_toml.get("precision", {}) + if not isinstance(precision_section, dict): raise ValueError("The [precision] section must be a TOML table.") - configure_precision(precision_config) + precision_mode = precision_section.get("mode", "full") + extra_keys = set(precision_section.keys()) - {"mode"} + if extra_keys: + logger.warning( + "Per-zone precision overrides are no longer supported and will be " + "ignored: %s. Edit jqmc/_precision.py directly to change zone " + "assignments.", + sorted(extra_keys), + ) + configure_precision(precision_mode) logger.info("") # default parameters diff --git a/jqmc_workflow/lrdmc_workflow.py b/jqmc_workflow/lrdmc_workflow.py index 9b578ac9..aac90004 100644 --- a/jqmc_workflow/lrdmc_workflow.py +++ b/jqmc_workflow/lrdmc_workflow.py @@ -326,7 +326,6 @@ def __init__( cleanup_patterns: Optional[list] = None, # -- [precision] section -- precision_mode: Optional[str] = None, - precision_overrides: Optional[dict] = None, ): super().__init__(cleanup_patterns=cleanup_patterns) self.server_machine_name = server_machine_name @@ -375,7 +374,6 @@ def __init__( self.max_continuation = max_continuation # [precision] section self.precision_mode = precision_mode - self.precision_overrides = precision_overrides @property def job_type(self) -> str: @@ -465,13 +463,8 @@ def _generate_input( jt: section_ov, } # Add [precision] section if configured - if self.precision_mode is not None or self.precision_overrides: - precision_ov = {} - if self.precision_mode is not None: - precision_ov["mode"] = self.precision_mode - if self.precision_overrides: - precision_ov.update(self.precision_overrides) - overrides["precision"] = precision_ov + if self.precision_mode is not None: + overrides["precision"] = {"mode": self.precision_mode} generate_input_toml( job_type=jt, overrides=overrides, diff --git a/jqmc_workflow/mcmc_workflow.py b/jqmc_workflow/mcmc_workflow.py index 21b4d849..fb49fe4c 100644 --- a/jqmc_workflow/mcmc_workflow.py +++ b/jqmc_workflow/mcmc_workflow.py @@ -249,7 +249,6 @@ def __init__( cleanup_patterns: Optional[list] = None, # -- [precision] section -- precision_mode: Optional[str] = None, - precision_overrides: Optional[dict] = None, ): super().__init__(cleanup_patterns=cleanup_patterns) self.server_machine_name = server_machine_name @@ -279,7 +278,6 @@ def __init__( self.max_continuation = max_continuation # [precision] section self.precision_mode = precision_mode - self.precision_overrides = precision_overrides # ── Input generation ────────────────────────────────────────── @@ -323,13 +321,8 @@ def _generate_input( "mcmc": mcmc_ov, } # Add [precision] section if configured - if self.precision_mode is not None or self.precision_overrides: - precision_ov = {} - if self.precision_mode is not None: - precision_ov["mode"] = self.precision_mode - if self.precision_overrides: - precision_ov.update(self.precision_overrides) - overrides["precision"] = precision_ov + if self.precision_mode is not None: + overrides["precision"] = {"mode": self.precision_mode} generate_input_toml( job_type="mcmc", overrides=overrides, diff --git a/jqmc_workflow/vmc_workflow.py b/jqmc_workflow/vmc_workflow.py index 2c00742e..403cb8dd 100644 --- a/jqmc_workflow/vmc_workflow.py +++ b/jqmc_workflow/vmc_workflow.py @@ -313,7 +313,6 @@ def __init__( cleanup_patterns: Optional[list] = None, # -- [precision] section -- precision_mode: Optional[str] = None, - precision_overrides: Optional[dict] = None, ): super().__init__(cleanup_patterns=cleanup_patterns) self.server_machine_name = server_machine_name @@ -358,7 +357,6 @@ def __init__( self.energy_slope_window_size = energy_slope_window_size # [precision] section self.precision_mode = precision_mode - self.precision_overrides = precision_overrides # ── Input generation ────────────────────────────────────────── @@ -427,13 +425,8 @@ def _generate_input( "vmc": vmc_ov, } # Add [precision] section if configured - if self.precision_mode is not None or self.precision_overrides: - precision_ov = {} - if self.precision_mode is not None: - precision_ov["mode"] = self.precision_mode - if self.precision_overrides: - precision_ov.update(self.precision_overrides) - overrides["precision"] = precision_ov + if self.precision_mode is not None: + overrides["precision"] = {"mode": self.precision_mode} generate_input_toml( job_type="vmc", overrides=overrides, diff --git a/tests/conftest.py b/tests/conftest.py index 2d67bc79..3ec4467f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,7 +35,7 @@ def configure_precision(request): from jqmc._precision import configure mode = request.config.getoption("--precision-mode") - configure({"mode": mode}) + configure(mode) def pytest_itemcollected(item): diff --git a/tests/test_mixed_precision.py b/tests/test_mixed_precision.py index 8fc992fd..0824a834 100644 --- a/tests/test_mixed_precision.py +++ b/tests/test_mixed_precision.py @@ -94,11 +94,11 @@ def _skip_if_not_mixed(request): @pytest.fixture(autouse=True) def _configure_mixed(): """Ensure mixed mode is active and JIT caches are cleared.""" - configure({"mode": "mixed"}) + configure("mixed") jax.clear_caches() yield # Restore full mode after each test to avoid polluting other tests - configure({"mode": "full"}) + configure("full") jax.clear_caches() From 050d593d38a268fe832d9dfe5adff6f22671df13 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sun, 26 Apr 2026 01:44:34 +0900 Subject: [PATCH 08/97] Refactored the mixed-precision implementation as selectable-precision modules with 3 design principles Brings all selectable-precision modules into compliance with the three design principles documented in `jqmc/_precision.py` P1: zone <-> owning module is 1:1; a module consults only its own zones. P2: a module may own multiple zones, named for purpose not dtype. P3a (frozen args): parameter names must not be rebound. Forwarding is dtype-neutral; cast at the arithmetic use site. P3b (local cast at point of arithmetic): cast operands to the function's own zone immediately before consumption. For catastrophic cancellation `(r - R)`: reconstruct in caller-supplied precision then `.astype(zone)`, never hardcode `jnp.float64` in the reconstruction. Main changes: - Remove parameter rebinds (`arg = jnp.asarray(arg, dtype=...)` at function entry) across `swct`, `atomic_orbital`, `hamiltonians`, `coulomb_potential`, `jastrow_factor`, and `determinant` (including JIT and debug helpers). Arguments are now forwarded dtype-neutral; casting happens at the arithmetic use site via `arg.astype(dtype_jnp)` or via new locals such as `A_old_inv_z`, `G_inv`, and `r_up_z` to avoid repeated casts without rebinding the parameter. - Replace hardcoded `jnp.float64` in `r - R` reconstructions with caller-supplied precision: `(r - R).astype(zone_dtype)` in `coulomb_potential`, `jastrow_factor`, and the `compute_AOs` / analytic grad-lap kernels in `atomic_orbital`. - Add explicit `.astype(get_dtype_jnp("mo_grad_lap"))` on AO-zone outputs before `jnp.dot(mo_coefficients, ...)` in `compute_MOs_laplacian`, `compute_MOs_grad`, and `_compute_MOs_grad_autodiff`. Runtime behavior is unchanged, since JAX promotion already produced the correct dtype; this change is for zone-leak auditability. - Extend the "no hardcoded dtypes" exemption in `_precision.py` to cover basis-data storage accessors (`_*_jnp` properties on dataclasses whose underlying field is `npt.NDArray[np.float64]`), with a one-line justification comment at each accessor body. These are lift-only NumPy-to-`jax.Array` adapters; storage is fp64 by construction. - Cosmetic: fix `molecular_orbital` module docstring zone names (`orb_eval` / `kinetic` -> `mo_eval` / `mo_grad_lap`) and relax `swct` debug-helper return type hints from `npt.NDArray[np.float64]` to plain `np.ndarray` for selectable-precision zones. Correct the `evaluate_swct_domega` JIT-function return type to `jax.Array`. --- .../run_pes_pipeline.py | 6 +- jqmc/_jqmc_utility.py | 8 +- jqmc/_precision.py | 463 +++++++++++++---- jqmc/atomic_orbital.py | 414 ++++++++------- jqmc/coulomb_potential.py | 386 +++++++------- jqmc/determinant.py | 407 ++++++++------- jqmc/hamiltonians.py | 38 +- jqmc/jastrow_factor.py | 481 ++++++++++-------- jqmc/jqmc_cli.py | 4 + jqmc/jqmc_gfmc.py | 189 ++++--- jqmc/jqmc_mcmc.py | 207 ++++---- jqmc/jqmc_tool.py | 13 +- jqmc/molecular_orbital.py | 75 +-- jqmc/structure.py | 65 ++- jqmc/swct.py | 71 ++- jqmc/trexio_wrapper.py | 33 +- jqmc/wavefunction.py | 256 ++++++---- jqmc_workflow/lrdmc_workflow.py | 7 +- jqmc_workflow/mcmc_workflow.py | 7 +- jqmc_workflow/vmc_workflow.py | 7 +- tests/test_AOs.py | 98 ++-- tests/test_MOs.py | 70 +-- tests/test_ao_basis_optimization.py | 57 ++- tests/test_determinant.py | 60 ++- tests/test_hamiltonian.py | 8 +- tests/test_jastrow.py | 84 +-- tests/test_jqmc_gfmc_bra.py | 4 +- tests/test_jqmc_gfmc_tau.py | 4 +- tests/test_jqmc_mcmc.py | 14 +- tests/test_jqmc_tool.py | 10 +- tests/test_lrdmc_force.py | 14 +- tests/test_mcmc_force.py | 8 +- tests/test_mixed_precision.py | 74 +-- tests/test_structure.py | 8 +- tests/test_swct.py | 2 +- tests/test_wave_function.py | 18 +- 36 files changed, 2079 insertions(+), 1591 deletions(-) diff --git a/examples/jqmc-workflow-example01/run_pes_pipeline.py b/examples/jqmc-workflow-example01/run_pes_pipeline.py index 46323bfd..82d948ad 100644 --- a/examples/jqmc-workflow-example01/run_pes_pipeline.py +++ b/examples/jqmc-workflow-example01/run_pes_pipeline.py @@ -51,9 +51,9 @@ TARGET_MCMC_ERROR = 5e-5 # Target statistical error (Ha) TARGET_LRDMC_ERROR = 5e-5 # Target statistical error (Ha) -# Mixed precision: set to "mixed" to enable float32 for low-risk zones, -# or None (default) for all-float64. See doc/notes/mixed_precision.md. -PRECISION_MODE = None # "mixed" or None +# Mixed precision: set to "mixed" to enable float32 for low-risk zones. +# Default "full" keeps all zones in float64. See doc/notes/mixed_precision.md. +PRECISION_MODE = "full" # "full" or "mixed" R_VALUES = [ 0.40, diff --git a/jqmc/_jqmc_utility.py b/jqmc/_jqmc_utility.py index 6ce532fc..7ed9925b 100644 --- a/jqmc/_jqmc_utility.py +++ b/jqmc/_jqmc_utility.py @@ -41,12 +41,9 @@ from functools import lru_cache from logging import getLogger -import jax.numpy as jnp import numpy as np import numpy.typing as npt -from ._precision import get_dtype - # set logger logger = getLogger("jqmc").getChild(__name__) @@ -102,8 +99,7 @@ def _generate_init_electron_configurations( min_dst = 0.1 max_dst = 1.0 - dtype = get_dtype("io") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = np.float64 # 1) zeta[i] = integer valence count per atom nion = coords.shape[0] @@ -405,7 +401,6 @@ def _cart_to_spherical_matrix(l: int) -> np.ndarray: ``A_sph = A_cart @ T`` under the normalization used in the codebase. Values are deterministic and cached to avoid runtime fitting. """ - precomputed: dict[int, np.ndarray] = { 0: np.array([[1.0]], dtype=np.float64), 1: np.array( @@ -731,7 +726,6 @@ def _spherical_to_cart_matrix(l: int) -> np.ndarray: Only ``_cart_to_spherical_matrix`` stores the full analytic values; this helper exposes the inverse direction for readability and reuse. """ - return _cart_to_spherical_matrix(l).T diff --git a/jqmc/_precision.py b/jqmc/_precision.py index fa5a83a2..6ff294cd 100644 --- a/jqmc/_precision.py +++ b/jqmc/_precision.py @@ -1,9 +1,160 @@ -"""Mixed precision configuration for jQMC. +r"""Mixed precision configuration for jQMC. + +================================================================================ +DESIGN PRINCIPLES (READ THIS FIRST) +================================================================================ + +The mixed-precision implementation rests on **three** principles. Principle 3 +is the most important in practice; almost every precision bug we have seen is +a violation of 3a or 3b. + +------------------------------------------------------------ +Principle 1 — One Precision Zone is owned by exactly one module. +------------------------------------------------------------ +A zone (e.g. ``ao_eval``, ``coulomb``) is *defined and consumed* in a single +module. The mapping zone ↔ owning module is one-to-one and is documented in +the table below (and enforced by convention in ``_FULL_PRECISION`` / +``_MIXED_PRECISION``). + +------------------------------------------------------------ +Principle 2 — A module may own multiple Precision Zones. +------------------------------------------------------------ +Different code paths in the same module legitimately need different precisions +(e.g. ``ao_eval`` vs ``ao_grad_lap``, or ``det_eval`` vs ``det_ratio``). Each +zone is named for its *purpose*, not for its dtype. + +------------------------------------------------------------ +Principle 3 — Cast responsibility lies with the function that does + arithmetic on the value, never with passthrough wrappers. +------------------------------------------------------------ + +Definition. *arithmetic* means consuming a value as an operand of a numerical +operation (``+ - * /``, ``jnp.linalg.norm``, ``jnp.dot``, ``@``, ``jnp.exp``, +…) **or** as an input to ``jax.grad`` / ``jax.jacrev`` / ``jax.hessian``. +Operations that do *not* count as arithmetic and therefore do *not* trigger a +cast: ``len(x)``, ``x.shape``, ``x[i]`` (index lookup), the *target* of +``.at[i].set(y)``, and forwarding ``x`` as an argument to another function. + +Principle 3a — Arguments are frozen. + Function arguments are treated as **frozen**, in the same sense as the + attributes of a ``@dataclass(frozen=True)``: the name introduced by the + parameter list **must not be rebound** for the entire body of the + function. In particular, ``arg = jnp.asarray(arg, dtype=...)`` at the + top of a function is forbidden — it silently coerces the argument for + every later use, including forwarding. + + Consequences (not extra rules — direct corollaries of "frozen"): + * Forwarding neutrality. A value forwarded to a callee transits in + the dtype it was received in; the callee is responsible for casting + it to *its* own zone (Principle 3b). + * Cast at the use site. When the function consumes ``arg`` as an + operand of its own arithmetic, the cast appears **inside the + expression** (``arg.astype(dtype)``). Do *not* preemptively + introduce a local alias just to hold the cast — only do so when + the cast result is reused multiple times, in which case introduce + a *new* local variable with a different name (e.g. + ``arg_local = arg.astype(dtype)``). The original ``arg`` always + remains frozen. + +Principle 3b — Local cast at the point of arithmetic. + A function casts a value to its own zone's dtype **immediately before** + consuming it as an operand. Inputs and outputs of the function's + arithmetic both live in its zone. Intermediate computations may use a + higher precision when needed for numerical reasons (the canonical case + being ``r - R``: reconstruct the difference in the **dtype the value + was received in** — i.e. the precision chosen by the upper layer — + to avoid catastrophic cancellation, then down-cast the result back to + the function's own zone). In jQMC the upstream (mcmc walker state) is + always fp64, so in practice the reconstruction happens in fp64; the + *principle*, however, is "use the caller-supplied precision," not + "hardcode fp64." Concretely: do not write ``jnp.float64`` as a + literal, and avoid pinning the reconstruction to a specific zone + name when the incoming value's own dtype already carries the right + precision. + +Worked example (the ECP → AO bug this design prevents):: + + # WRONG: rebinding `r_carts` at the top of compute_coulomb forwards a + # fp32-truncated array to compute_AOs, even though `ao_eval` is fp64. + def compute_coulomb(r_carts, R_carts): + dtype_jnp = get_dtype_jnp("coulomb") + r_carts = jnp.asarray(r_carts, dtype=dtype_jnp) # 3a violation + R_carts = jnp.asarray(R_carts, dtype=dtype_jnp) + ao = compute_AOs(..., r_carts, R_carts) # downstream sees fp32 + diff = r_carts - R_carts + ... + + # RIGHT: forwarding stays in the caller's dtype; the local arithmetic + # reconstructs the difference in the dtype the values were received in + # (the upper-layer precision — fp64 in jQMC because mcmc walker state + # is fp64) and casts the result back to the function's own zone. + def compute_coulomb(r_carts, R_carts): + ao = compute_AOs(..., r_carts, R_carts) # 3a: forward as-is + dtype_jnp = get_dtype_jnp("coulomb") + # reconstruct in the caller-supplied precision, then down-cast + diff = (r_carts - R_carts).astype(dtype_jnp) # 3b + ... -Every computational function declares its Precision Zone and explicitly -specifies dtype for all variables it creates or consumes. This design -does NOT rely on JAX's implicit dtype propagation, ensuring robustness -against future changes in JAX's type promotion semantics. +Auditing recipe. + To verify a module: + * (3a) Search for ``arg = jnp.asarray(arg, dtype=...)`` at the top of + any public function. Each occurrence is a 3a candidate violation — + the rebind silently coerces the argument for any subsequent + forwarding too. + * (3b) For each arithmetic expression, check that all operands have + been cast to the function's zone in the immediately preceding lines. + * (catastrophic cancellation) For each ``r - R`` style difference of + coordinates, check the reconstruct-in-caller-precision-then-downcast + pattern (in jQMC this is fp64 in practice because the upstream is + always fp64, but the rule is "use the dtype the value was received + in," not "hardcode fp64"). + +No hardcoded dtypes inside selectable-precision modules. + Inside any module that owns one or more selectable-precision zones + (i.e. any zone whose dtype can differ between ``"full"`` and + ``"mixed"`` mode), **never hardcode** ``jnp.float64`` / ``np.float64`` + / ``jnp.float32`` / ``np.float32`` as a literal dtype for arrays the + module produces or consumes. Always go through the accessors + ``get_dtype_jnp("")`` / ``get_dtype_np("")`` so the dtype + follows the active mode automatically. The only legitimate exception + is a module whose owned data is **always fp64 by construction**, + independent of any selectable zone: + + * ``mcmc`` / ``gfmc`` (MCMC and GFMC walker state, always fp64). + * I/O modules that load and store external numerical data + (``structure``, ``trexio_wrapper``, ``_jqmc_utility``, + ``jqmc_tool``, and the ``_load_dataclass_from_hdf5`` / + ``_save_dataclass_to_hdf5`` helpers in ``hamiltonians``): + on-disk numerical data (AO exponents/coefficients, nuclear + coordinates, geminal coefficients, etc.) is always fp64 + because fp32 storage would silently lose precision that no + downstream fp64 upcast can recover. + * Basis-data storage accessors. ``_*_jnp`` properties on + selectable-precision dataclasses whose underlying storage + field is typed ``npt.NDArray[np.float64]`` are *lift-only* + adapters (numpy → ``jax.Array``), not arithmetic. The dtype + is fp64 by construction: storage is loaded from + HDF5/TREXIO/optimizer output (see Phase A1 numpy-storage + migration), and downcasting at the accessor would silently + lose precision that no downstream upcast can recover. The + consumer is responsible for casting the lifted array to its + own zone at the use site (Principle 3b). Concretely this + covers the ``_*_jnp`` accessors for AO exponents/coefficients + and normalization-factorial-ratio caches in + ``atomic_orbital``, ``_mo_coefficients_jnp`` in + ``molecular_orbital``, ``_lambda_matrix_jnp`` in + ``determinant``, ``_j_matrix_jnp`` in ``jastrow_factor``, + and the ``ShellPrimMap.from_aos_data`` constructor in + ``atomic_orbital``. + + These modules may use ``jnp.float64`` / ``np.float64`` directly + because the dtype is not a function of ``mode``. Audit with:: + + grep -nE 'jnp\.float(32|64)|np\.float(32|64)' jqmc/.py + + Each hit inside a selectable-precision module is a candidate violation. + +================================================================================ Users choose one of two modes: @@ -19,46 +170,65 @@ Precision Zones --------------- -============== ============================ ========= ======== ============ -Zone Components Default Mixed float32 risk -============== ============================ ========= ======== ============ -``orb_eval`` AO/MO forward evaluation float64 float32 low -``jastrow`` Jastrow factor (J1/J2/J3) float64 float32 low -``geminal`` Geminal matrix elements float64 float64 high -``determinant`` log-det, SVD, AS reg. float64 float64 high -``coulomb`` Coulomb + ECP potential float64 float32 low-medium -``kinetic`` Kinetic energy + AO/MO derivs float64 float64 high -``mcmc`` MCMC sampling float64 float64 high -``gfmc`` GFMC propagation float64 float64 high -``optimization``SR matrix, parameter updates float64 float64 high -``io`` I/O, structure data float64 float64 low-medium -============== ============================ ========= ======== ============ - -File-to-zone mapping --------------------- - -- ``atomic_orbital.py``: ``orb_eval`` (forward), ``kinetic`` (grad/laplacian) -- ``molecular_orbital.py``: ``orb_eval`` (forward), ``kinetic`` (grad/laplacian) -- ``jastrow_factor.py``: ``jastrow`` (forward), ``kinetic`` (grad/laplacian), - ``mcmc`` (ratio/update) -- ``determinant.py``: ``geminal`` (matrix elements), ``determinant`` (log-det/SVD), - ``kinetic`` (grad/laplacian) -- ``coulomb_potential.py``: ``coulomb`` -- ``wavefunction.py``: ``kinetic`` + zone-boundary casts -- ``hamiltonians.py``: ``kinetic`` (zone-boundary aggregation of T + V) -- ``jqmc_mcmc.py``: ``mcmc`` (sampling), ``optimization`` (SR/LM) -- ``jqmc_gfmc.py``: ``gfmc`` -- ``structure.py``, ``trexio_wrapper.py``, ``jqmc_tool.py``: ``io`` -- ``_jqmc_utility.py``: ``io`` -- ``swct.py``: ``kinetic`` +================== ================================= ========= ======== ===== ========= +Zone Owning module Default Mixed risk E_L path +================== ================================= ========= ======== ===== ========= +``ao_eval`` atomic_orbital.py (forward) float64 float32 low core +``ao_grad_lap`` atomic_orbital.py (grad/lap) float64 float32 low core +``mo_eval`` molecular_orbital.py (forward) float64 float64 high* core +``mo_grad_lap`` molecular_orbital.py (grad/lap) float64 float64 high core +``jastrow_eval`` jastrow_factor.py (forward) float64 float32 low core† +``jastrow_grad_lap`` jastrow_factor.py (grad/lap) float64 float32 low core +``jastrow_ratio`` jastrow_factor.py (ratio update) float64 float32 low indirect‡ +``det_eval`` determinant.py (geminal + log-det) float64 float64 high core +``det_grad_lap`` determinant.py (grad/lap of lnDet) float64 float64 high core +``det_ratio`` determinant.py (SM ratio update) float64 float64 high indirect‡ +``coulomb`` coulomb_potential.py float64 float32 low-med core +``wf_eval`` wavefunction.py (Psi, ln Psi) float64 float64 high core† +``wf_kinetic`` wavefunction.py (T_L assembly) float64 float64 high core +``wf_ratio`` wavefunction.py (Psi(R')/Psi(R)) float64 float64 high no +``local_energy`` hamiltonians.py (T + V assembly) float64 float64 high core +``swct`` swct.py float64 float64 high no +================== ================================= ========= ======== ===== ========= + +\\* ``mo_eval`` is a high-risk zone even though the consumed AO values are +fp32: the small ``mo_coefficients @ aos`` matmul is run in this zone, and +its output feeds the determinant matrix, where fp32 round-off is +amplified by log|det|. See ``bug/fp32`` diagnostics. + +† ``jastrow_eval`` and ``wf_eval`` are on the E_L core path but their +forward values (J and ln|Psi|) do not enter the E_L formula directly +(E_L depends on *derivatives* of ln|Psi|). Diagnostics show zero E_L +bias when these zones alone are fp32. + +‡ ``det_ratio`` and ``jastrow_ratio`` affect E_L **indirectly** through +the ECP non-local potential, which evaluates Psi(R')/Psi(R) on a +quadrature grid via rank-1 ratio updates (see +``coulomb_potential.compute_ecp_non_local_parts_nearest_neighbors_fast_update``). +In non-ECP systems these zones have no E_L impact. Usage:: - from jqmc._precision import get_dtype + from jqmc._precision import get_dtype_jnp def compute_AOs(aos_data, r_carts): - dtype = get_dtype("orb_eval") - r_carts = jnp.asarray(r_carts, dtype=dtype) + # Forwarding-only path: do NOT rebind r_carts here (Principle 3a). + return _compute_AOs_kernel(aos_data, r_carts) + + def _compute_AOs_kernel(aos_data, r_carts): + # This function performs arithmetic on r_carts, so it casts at the + # use site (Principle 3b). The (r - R) reconstruction is done in + # the dtype the values were received in (caller-supplied + # precision: fp64 in jQMC because mcmc walker state is fp64 and + # the atomic centers are loaded from disk as fp64). The result + # is then down-cast to this function's own zone (``ao_eval``). + # NOTE: never reach for another module's zone (e.g. + # ``get_dtype_jnp("local_energy")``) here — that violates + # Principle 1 (zone ↔ owning module is 1:1). atomic_orbital.py + # may only consult ao_eval / ao_grad_lap. + dtype_jnp = get_dtype_jnp("ao_eval") + R_carts = aos_data._atomic_center_carts_jnp + diff = (r_carts - R_carts).astype(dtype_jnp) ... """ @@ -97,77 +267,130 @@ def compute_AOs(aos_data, r_carts): import logging import jax.numpy as jnp +import numpy as np logger = logging.getLogger(__name__) # --- mode="full" (all float64, backward compatible) --- +# Zones are listed grouped by owning module for readability. _FULL_PRECISION: dict[str, str] = { - "orb_eval": "float64", # AO/MO forward evaluation - "jastrow": "float64", # Jastrow factor - "geminal": "float64", # Geminal matrix elements - "determinant": "float64", # log-det, SVD, AS regularization + # atomic_orbital.py + "ao_eval": "float64", # AO forward evaluation + "ao_grad_lap": "float64", # AO gradient / Laplacian + # molecular_orbital.py + "mo_eval": "float64", # MO forward evaluation (mo_coef @ AO) + "mo_grad_lap": "float64", # MO gradient / Laplacian + # jastrow_factor.py + "jastrow_eval": "float64", # Jastrow factor (J1/J2/J3) + "jastrow_grad_lap": "float64", # Jastrow gradient / Laplacian + "jastrow_ratio": "float64", # Jastrow ratio (rank-1 update) + # determinant.py + "det_eval": "float64", # geminal matrix elements + log-det / SVD / AS reg + "det_grad_lap": "float64", # gradient / Laplacian of ln|Det| + "det_ratio": "float64", # |Det(R')|/|Det(R)| Sherman-Morrison rank-1 update + # coulomb_potential.py "coulomb": "float64", # Coulomb + ECP potential - "kinetic": "float64", # Kinetic energy + AO/MO derivatives - "mcmc": "float64", # MCMC sampling (proposal, SM update, accept/reject, accumulation) - "gfmc": "float64", # GFMC propagation - "optimization": "float64", # SR matrix, parameter updates - "io": "float64", # I/O, structure data + # wavefunction.py + "wf_eval": "float64", # Psi, ln Psi evaluators + "wf_kinetic": "float64", # T_L = -1/2 (lap_lnPsi + |grad_lnPsi|^2) assembly + "wf_ratio": "float64", # Psi(R')/Psi(R) = exp(J' - J) * det'/det (LRDMC discretized) + # hamiltonians.py + "local_energy": "float64", # E_L = T + V assembly + # swct.py + "swct": "float64", # SWCT omega / domega } # --- mode="mixed" (recommended mixed precision) --- -# float32 risk: -# orb_eval - low: smooth Gaussian basis + linear combination. -# (Heavy AO eval stays in fp32; compute_MOs upcasts the small -# matmul to the determinant zone, see molecular_orbital.py.) -# jastrow - low: smooth correlation function, pre-exp value -# geminal - HIGH: this matrix is the input to LU/det; even ε≈1e-7 on -# entries amplifies into log|det| errors of O(1) for ~32x32 -# systems with non-trivial condition numbers -# (see bug/fp32 diagnostics). Kept in fp64. -# coulomb - low-medium: sum of 1/r + ECP spherical quadrature -# determinant - high: log(det) cancellation, SVD 1/s, eigenvalue ops -# kinetic - high: second derivative of ln|Psi|, cancellation-sensitive -# mcmc - high: SM inverse error accumulation, acceptance ratio, statistics -# gfmc - high: weighted branching/pruning, population collapse in float32 -# optimization- high: S^{-1}F linear system, ill-conditioned matrix -# io - low-medium: file I/O + nuclear coordinates +# Five "low risk" zones drop to float32: +# +# ao_eval - smooth Gaussian basis kernel; the dominant cost. +# The downstream consumer (mo_eval / det_eval / +# jastrow_eval) is fp64 and explicitly casts the AO +# result up before any sensitive arithmetic. +# ao_grad_lap - AO gradient / Laplacian kernel; same O(N_ao × N_e) +# cost as ao_eval. Diagnostics show bias < 6e-05 Ha +# at 32 electrons (0.05 kcal/mol margin ×1.3). +# jastrow_eval - smooth correlation function value (pre-exp). +# jastrow_grad_lap - nabla J, nabla^2 J; Jastrow is a smooth function +# with low cancellation. Diagnostics show bias +# < 8e-06 Ha at 32 electrons (0.05 kcal/mol margin ×11). +# jastrow_ratio - J(R')-J(R) log-ratio; smooth and well-behaved. +# Diagnostics show bias < 2e-06 Ha (margin ×44). +# +# All other zones stay fp64 because numerical experiments (see +# bug/fp32 diagnostics) show fp32 in those zones produces +# unacceptable bias on E_L for ~32-electron systems, OR the +# kernel is cheap enough that fp32 is not worth the bias: +# +# coulomb - sum of 1/r + ECP spherical quadrature. Cheap +# (O(N_e^2) el-el + O(N_e * N_nuc) el-ion, vs +# O(N_e * N_ao) AO eval) but contributes the +# largest individual bias among fp32 candidates +# (~6e-5 Ha at 64e/512 AO). Cost/benefit favors fp64. +# +# mo_eval - mo_coef @ AO matmul feeds the determinant matrix; +# fp32 here amplifies into log|det| errors of O(1). +# det_eval - geminal matrix + log(det) + SVD; cancellation in +# log(det), SVD 1/s near-singular, ε≈1e-7 entries +# produce O(1) log|det| error. +# *_grad_lap - second derivatives of ln|Psi|; cancellation-sensitive +# (except ao_grad_lap and jastrow_grad_lap — smooth kernels). +# wf_kinetic - sum (lap_J + lap_lnD) + |grad_J + grad_lnD|^2; cancellation. +# local_energy - T + V assembly; small differences between large terms. +# det_ratio - SM rank-1 ratio used by MCMC accept/reject AND +# ECP non-local Psi(R')/Psi(R) quadrature. +# swct - geometric SWCT correction, derivative-sensitive. _MIXED_PRECISION: dict[str, str] = { - "orb_eval": "float32", # low risk (heavy kernel only) - "jastrow": "float32", # low risk - "geminal": "float64", # high risk: feeds LU/det - "determinant": "float64", # high risk - "coulomb": "float32", # low-medium risk - "kinetic": "float64", # high risk - "mcmc": "float64", # high risk - "gfmc": "float64", # high risk - "optimization": "float64", # high risk - "io": "float64", # low-medium risk + # atomic_orbital.py + "ao_eval": "float32", # low risk (heavy kernel) + "ao_grad_lap": "float32", # low risk (bias < 6e-05 Ha at 32e; heavy kernel) + # molecular_orbital.py + "mo_eval": "float64", # high risk (feeds det_eval) + "mo_grad_lap": "float64", # high risk + # jastrow_factor.py + "jastrow_eval": "float32", # low risk + "jastrow_grad_lap": "float32", # low risk (smooth J; bias < 8e-06 Ha at 32e) + "jastrow_ratio": "float32", # low risk (smooth J ratio; bias < 2e-06 Ha at 32e) + # determinant.py + "det_eval": "float64", # high risk (LU/det / SVD) + "det_grad_lap": "float64", # high risk + "det_ratio": "float64", # high risk (SM update error + ECP non-local ratio) + # coulomb_potential.py + "coulomb": "float64", # cheap kernel + largest single fp32 bias (~6e-5 Ha) + # wavefunction.py + "wf_eval": "float64", # high risk + "wf_kinetic": "float64", # high risk + "wf_ratio": "float64", # high risk (exp(J'-J)*det'/det in LRDMC) + # hamiltonians.py + "local_energy": "float64", # high risk + # swct.py + "swct": "float64", # high risk } ALL_ZONES = frozenset(_FULL_PRECISION.keys()) -# Runtime zone -> dtype mapping -_zone_dtypes: dict[str, type] = {} +# Runtime zone -> dtype string mapping ("float32" or "float64"). +# Strings are stored (not numpy/jax dtype types) so the str -> jnp.* / np.* +# conversion lives inside the per-flavor accessors below. This keeps the +# concrete dtype flavor (jnp vs np) cleanly separated at the API boundary. +_zone_dtypes: dict[str, str] = {} -def _str_to_dtype(s: str) -> type: - """Convert a string dtype name to the corresponding JAX/NumPy dtype type. +def _validate_dtype_str(s: str) -> str: + """Validate that *s* is one of the accepted dtype strings. Args: s: Either ``"float32"`` or ``"float64"``. Returns: - ``jnp.float32`` or ``jnp.float64``. + The same string, unchanged. Raises: ValueError: If *s* is not one of the two accepted strings. """ - if s == "float32": - return jnp.float32 - elif s == "float64": - return jnp.float64 - else: + if s not in ("float32", "float64"): raise ValueError(f"Invalid dtype '{s}'. Must be 'float32' or 'float64'.") + return s def configure(mode: str = "full") -> None: @@ -190,9 +413,7 @@ def configure(mode: str = "full") -> None: raise ValueError(f"Unknown precision mode '{mode}'. Must be 'full' or 'mixed'.") for zone, dtype_str in base.items(): - _zone_dtypes[zone] = _str_to_dtype(dtype_str) - - logger.info(summary()) + _zone_dtypes[zone] = _validate_dtype_str(dtype_str) def _set_zone(zone: str, dtype_str: str) -> None: @@ -209,32 +430,52 @@ def _set_zone(zone: str, dtype_str: str) -> None: """ if zone not in ALL_ZONES: raise ValueError(f"Unknown precision zone '{zone}'. Available zones: {sorted(ALL_ZONES)}") - _zone_dtypes[zone] = _str_to_dtype(dtype_str) + _zone_dtypes[zone] = _validate_dtype_str(dtype_str) -def get_dtype(zone: str) -> type: - """Return the dtype for a Precision Zone. +def _get_zone_str(zone: str) -> str: + """Return the stored dtype string for *zone* (``"float32"`` or ``"float64"``). - When :func:`configure` has not been called, ``jnp.float64`` is returned + When :func:`configure` has not been called, ``"float64"`` is returned for any zone name (backward compatible). After :func:`configure` has been called, an unknown *zone* raises :class:`ValueError`. + """ + if _zone_dtypes: + if zone not in ALL_ZONES: + raise ValueError(f"Unknown precision zone '{zone}'. Available zones: {sorted(ALL_ZONES)}") + return _zone_dtypes.get(zone, "float64") + + +def get_dtype_jnp(zone: str) -> type: + """Return the JAX dtype for a Precision Zone. Args: - zone: Precision Zone name (e.g. ``"orb_eval"``, ``"kinetic"``). + zone: Precision Zone name (e.g. ``"ao_eval"``, ``"wf_kinetic"``). Returns: ``jnp.float32`` or ``jnp.float64``. """ - if _zone_dtypes: - # configure() has been called: validate zone name - if zone not in ALL_ZONES: - raise ValueError(f"Unknown precision zone '{zone}'. Available zones: {sorted(ALL_ZONES)}") - return _zone_dtypes.get(zone, jnp.float64) + return jnp.float32 if _get_zone_str(zone) == "float32" else jnp.float64 + + +def get_dtype_np(zone: str) -> type: + """Return the numpy dtype for a Precision Zone. + + Convenience helper for numpy-only code paths (e.g. ``_debug`` reference + implementations) where importing or branching on ``jnp`` is awkward. + + Args: + zone: Precision Zone name (e.g. ``"ao_eval"``, ``"wf_kinetic"``). + + Returns: + ``np.float32`` or ``np.float64``. + """ + return np.float32 if _get_zone_str(zone) == "float32" else np.float64 def is_mixed_precision_enabled() -> bool: """Return ``True`` if at least one zone is set to float32.""" - return any(d == jnp.float32 for d in _zone_dtypes.values()) + return any(s == "float32" for s in _zone_dtypes.values()) def get_tolerance(zone: str, level: str = "strict") -> tuple[float, float]: @@ -249,8 +490,7 @@ def get_tolerance(zone: str, level: str = "strict") -> tuple[float, float]: """ from jqmc._setting import _TOLERANCE - dtype_key = "float32" if get_dtype(zone) == jnp.float32 else "float64" - return _TOLERANCE[level][dtype_key] + return _TOLERANCE[level][_get_zone_str(zone)] def get_tolerance_min(zones, level: str = "strict") -> tuple[float, float]: @@ -271,11 +511,20 @@ def get_tolerance_min(zones, level: str = "strict") -> tuple[float, float]: return max(atols), max(rtols) -def summary() -> str: - """Return a human-readable summary of the current precision configuration.""" - lines = ["Precision configuration:"] +def mode_label() -> str: + """Return a short label for the active precision mode. + + Returns: + ``"Full Precision (FP64)"`` or ``"Mixed Precision (FP32 + FP64)"``. + """ + if is_mixed_precision_enabled(): + return "Mixed Precision (FP32 + FP64)" + return "Full Precision (FP64)" + + +def zone_detail() -> str: + """Return a per-zone detail string of the current precision configuration.""" + lines = [] for zone in sorted(ALL_ZONES): - dtype = get_dtype(zone) - tag = "float32" if dtype == jnp.float32 else "float64" - lines.append(f" {zone}: {tag}") + lines.append(f" {zone}: {_get_zone_str(zone)}") return "\n".join(lines) diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index 62b5af70..b91c3148 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -57,8 +57,8 @@ from numpy import linalg as LA from ._jqmc_utility import _spherical_to_cart_matrix -from ._precision import get_dtype -from ._setting import EPS_stabilizing_jax_AO_cart_deriv, atol_consistency, get_eps, rtol_consistency +from ._precision import get_dtype_jnp, get_dtype_np +from ._setting import atol_consistency, get_eps, rtol_consistency from .structure import Structure_data # set logger @@ -86,8 +86,8 @@ class AOs_cart_data: num_ao (int): Number of contracted AOs. num_ao_prim (int): Number of primitive Gaussians. orbital_indices (list[int] | tuple[int]): For each primitive, the parent AO index (``len == num_ao_prim``). - exponents (list[float] | tuple[float]): Gaussian exponents for primitives (``len == num_ao_prim``). - coefficients (list[float] | tuple[float]): Contraction coefficients per primitive (``len == num_ao_prim``). + exponents (npt.NDArray[np.float64]): Gaussian exponents for primitives (``len == num_ao_prim``). dtype: float64. + coefficients (npt.NDArray[np.float64]): Contraction coefficients per primitive (``len == num_ao_prim``). dtype: float64. angular_momentums (list[int] | tuple[int]): Angular momentum quantum numbers ``l`` per AO (``len == num_ao``). polynominal_order_x (list[int] | tuple[int]): Cartesian power ``n_x`` for each AO (``len == num_ao``). polynominal_order_y (list[int] | tuple[int]): Cartesian power ``n_y`` for each AO (``len == num_ao``). @@ -230,10 +230,12 @@ class AOs_cart_data: num_ao_prim: int = struct.field(pytree_node=False, default=0) #: For each primitive, the parent AO index (``len == num_ao_prim``). orbital_indices: list[int] | tuple[int] = struct.field(pytree_node=False, default_factory=tuple) - #: Gaussian exponents for primitives (``len == num_ao_prim``). - exponents: jax.Array = struct.field(pytree_node=True, default_factory=lambda: jnp.array([])) - #: Contraction coefficients per primitive (``len == num_ao_prim``). - coefficients: jax.Array = struct.field(pytree_node=True, default_factory=lambda: jnp.array([])) + #: Gaussian exponents for primitives (``len == num_ao_prim``). dtype: float64. + exponents: npt.NDArray[np.float64] = struct.field(pytree_node=True, default_factory=lambda: np.array([], dtype=np.float64)) + #: Contraction coefficients per primitive (``len == num_ao_prim``). dtype: float64. + coefficients: npt.NDArray[np.float64] = struct.field( + pytree_node=True, default_factory=lambda: np.array([], dtype=np.float64) + ) #: Angular momentum quantum numbers ``l`` per AO (``len == num_ao``). angular_momentums: list[int] | tuple[int] = struct.field(pytree_node=False, default_factory=tuple) #: Cartesian power ``n_x`` for each AO (``len == num_ao``). @@ -287,10 +289,10 @@ def sanity_check(self) -> None: raise ValueError(f"num_ao_prim = {type(self.num_ao_prim)} must be an int.") if not isinstance(self.orbital_indices, (tuple, list)): raise ValueError(f"orbital_indices = {type(self.orbital_indices)} must be a list or tuple.") - if not isinstance(self.exponents, (tuple, list, jax.Array, np.ndarray)): - raise ValueError(f"exponents = {type(self.exponents)} must be a jax.Array, np.ndarray, list, or tuple.") - if not isinstance(self.coefficients, (tuple, list, jax.Array, np.ndarray)): - raise ValueError(f"coefficients = {type(self.coefficients)} must be a jax.Array, np.ndarray, list, or tuple.") + if not isinstance(self.exponents, np.ndarray): + raise ValueError(f"exponents = {type(self.exponents)} must be an np.ndarray (float64).") + if not isinstance(self.coefficients, np.ndarray): + raise ValueError(f"coefficients = {type(self.coefficients)} must be an np.ndarray (float64).") if not isinstance(self.angular_momentums, (tuple, list)): raise ValueError(f"angular_momentums = {type(self.angular_momentums)} must be a list or tuple.") if not isinstance(self.polynominal_order_x, (tuple, list)): @@ -431,8 +433,8 @@ def _build_uncontracted_aos(self) -> "AOs_cart_data": num_ao=len(new_nucleus_index), num_ao_prim=len(new_orbital_indices), orbital_indices=new_orbital_indices, - exponents=new_exponents, - coefficients=new_coefficients, + exponents=np.array(new_exponents, dtype=np.float64), + coefficients=np.array(new_coefficients, dtype=np.float64), angular_momentums=new_angular_momentums, polynominal_order_x=new_polynominal_order_x, polynominal_order_y=new_polynominal_order_y, @@ -707,7 +709,9 @@ def _polynominal_order_z_prim_jnp(self) -> jax.Array: @property def _normalization_factorial_ratio_prim_jnp(self) -> jax.Array: """Return factorial ratio used in AO normalization (primitive-wise).""" - dtype = get_dtype("io") + # Lift-only fp64 basis-data storage accessor (see _precision.py exemption); + # consumer casts to its own zone at use site. + dtype_jnp = jnp.float64 nx = self._polynominal_order_x_prim_np ny = self._polynominal_order_y_prim_np nz = self._polynominal_order_z_prim_np @@ -721,21 +725,25 @@ def _normalization_factorial_ratio_prim_jnp(self) -> jax.Array: * scipy.special.factorial(2 * ny, exact=True) * scipy.special.factorial(2 * nz, exact=True) ) - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = np.float64 ratio = np.asarray(num / den, dtype=dtype_np) - return jnp.array(ratio, dtype=dtype) + return jnp.array(ratio, dtype=dtype_jnp) @property def _exponents_jnp(self) -> jax.Array: """Return exponents.""" - dtype = get_dtype("io") - return jnp.asarray(self.exponents, dtype=dtype) + # Lift-only fp64 basis-data storage accessor (see _precision.py exemption); + # consumer casts to its own zone at use site. + dtype_jnp = jnp.float64 + return jnp.asarray(self.exponents, dtype=dtype_jnp) @property def _coefficients_jnp(self) -> jax.Array: """Return coefficients.""" - dtype = get_dtype("io") - return jnp.asarray(self.coefficients, dtype=dtype) + # Lift-only fp64 basis-data storage accessor (see _precision.py exemption); + # consumer casts to its own zone at use site. + dtype_jnp = jnp.float64 + return jnp.asarray(self.coefficients, dtype=dtype_jnp) @property def _num_orb(self) -> int: @@ -756,8 +764,8 @@ class AOs_sphe_data: num_ao (int): Number of contracted AOs. num_ao_prim (int): Number of primitive Gaussians. orbital_indices (list[int] | tuple[int]): For each primitive, the parent AO index (``len == num_ao_prim``). - exponents (list[float] | tuple[float]): Gaussian exponents for primitives (``len == num_ao_prim``). - coefficients (list[float] | tuple[float]): Contraction coefficients per primitive (``len == num_ao_prim``). + exponents (npt.NDArray[np.float64]): Gaussian exponents for primitives (``len == num_ao_prim``). dtype: float64. + coefficients (npt.NDArray[np.float64]): Contraction coefficients per primitive (``len == num_ao_prim``). dtype: float64. angular_momentums (list[int] | tuple[int]): Angular momentum quantum numbers ``l`` per AO (``len == num_ao``). magnetic_quantum_numbers (list[int] | tuple[int]): Magnetic quantum numbers ``m`` per AO (``len == num_ao``), satisfying ``-l <= m <= l``. @@ -883,10 +891,12 @@ class AOs_sphe_data: num_ao_prim: int = struct.field(pytree_node=False, default=0) #: For each primitive, the parent AO index (``len == num_ao_prim``). orbital_indices: list[int] | tuple[int] = struct.field(pytree_node=False, default_factory=tuple) - #: Gaussian exponents for primitives (``len == num_ao_prim``). - exponents: jax.Array = struct.field(pytree_node=True, default_factory=lambda: jnp.array([])) - #: Contraction coefficients per primitive (``len == num_ao_prim``). - coefficients: jax.Array = struct.field(pytree_node=True, default_factory=lambda: jnp.array([])) + #: Gaussian exponents for primitives (``len == num_ao_prim``). dtype: float64. + exponents: npt.NDArray[np.float64] = struct.field(pytree_node=True, default_factory=lambda: np.array([], dtype=np.float64)) + #: Contraction coefficients per primitive (``len == num_ao_prim``). dtype: float64. + coefficients: npt.NDArray[np.float64] = struct.field( + pytree_node=True, default_factory=lambda: np.array([], dtype=np.float64) + ) #: Angular momentum quantum numbers ``l`` per AO (``len == num_ao``). angular_momentums: list[int] | tuple[int] = struct.field(pytree_node=False, default_factory=tuple) #: Magnetic quantum numbers ``m`` per AO (``len == num_ao``; ``-l <= m <= l``). @@ -929,10 +939,10 @@ def sanity_check(self) -> None: raise ValueError(f"num_ao_prim = {type(self.num_ao_prim)} must be an int.") if not isinstance(self.orbital_indices, (list, tuple)): raise ValueError(f"orbital_indices = {type(self.orbital_indices)} must be a list or tuple.") - if not isinstance(self.exponents, (list, tuple, jax.Array, np.ndarray)): - raise ValueError(f"exponents = {type(self.exponents)} must be a jax.Array, np.ndarray, list, or tuple.") - if not isinstance(self.coefficients, (list, tuple, jax.Array, np.ndarray)): - raise ValueError(f"coefficients = {type(self.coefficients)} must be a jax.Array, np.ndarray, list, or tuple.") + if not isinstance(self.exponents, np.ndarray): + raise ValueError(f"exponents = {type(self.exponents)} must be an np.ndarray (float64).") + if not isinstance(self.coefficients, np.ndarray): + raise ValueError(f"coefficients = {type(self.coefficients)} must be an np.ndarray (float64).") if not isinstance(self.angular_momentums, (list, tuple)): raise ValueError(f"angular_momentums = {type(self.angular_momentums)} must be a list or tuple.") if not isinstance(self.magnetic_quantum_numbers, (list, tuple)): @@ -1051,8 +1061,8 @@ def _build_uncontracted_aos(self) -> "AOs_sphe_data": num_ao=len(new_nucleus_index), num_ao_prim=len(new_orbital_indices), orbital_indices=new_orbital_indices, - exponents=new_exponents, - coefficients=new_coefficients, + exponents=np.array(new_exponents, dtype=np.float64), + coefficients=np.array(new_coefficients, dtype=np.float64), angular_momentums=new_angular_momentums, magnetic_quantum_numbers=new_magnetic_quantum_numbers, ) @@ -1278,14 +1288,18 @@ def _magnetic_quantum_numbers_prim_jnp(self) -> jax.Array: @property def _exponents_jnp(self) -> jax.Array: """Return exponents.""" - dtype = get_dtype("io") - return jnp.asarray(self.exponents, dtype=dtype) + # Lift-only fp64 basis-data storage accessor (see _precision.py exemption); + # consumer casts to its own zone at use site. + dtype_jnp = jnp.float64 + return jnp.asarray(self.exponents, dtype=dtype_jnp) @property def _coefficients_jnp(self) -> jax.Array: """Return coefficients.""" - dtype = get_dtype("io") - return jnp.asarray(self.coefficients, dtype=dtype) + # Lift-only fp64 basis-data storage accessor (see _precision.py exemption); + # consumer casts to its own zone at use site. + dtype_jnp = jnp.float64 + return jnp.asarray(self.coefficients, dtype=dtype_jnp) @property def _num_orb(self) -> int: @@ -1352,8 +1366,9 @@ def symmetrize(self, arr: np.ndarray) -> np.ndarray: @classmethod def from_aos_data(cls, aos_data: "AOs_sphe_data | AOs_cart_data") -> "ShellPrimMap": """Build a shell map from an AO dataclass instance.""" - dtype = get_dtype("io") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + # Build-time copy of fp64 basis-data storage (see _precision.py exemption for + # basis-data storage accessors); used only for shell identity and indexing. + dtype_np = np.float64 ao_prims: dict[int, list[int]] = {} for prim_idx, ao_idx in enumerate(aos_data.orbital_indices): ao_prims.setdefault(ao_idx, []).append(prim_idx) @@ -1420,8 +1435,7 @@ def _aos_sphe_to_cart(aos_data: AOs_sphe_data | AOs_cart_data) -> tuple[AOs_cart tuple: (AOs_cart_data, transform_matrix) where transform_matrix maps spherical -> Cartesian coefficients with shape (num_ao_sph, num_ao_cart). """ - dtype = get_dtype("orb_eval") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("ao_eval") if isinstance(aos_data, AOs_cart_data): transform_matrix = np.eye(aos_data.num_ao, dtype=dtype_np) return aos_data, transform_matrix @@ -1501,8 +1515,8 @@ def _match_shell(existing: dict, nucleus: int, l: int, exps: np.ndarray, coefs: num_ao=total_cart, num_ao_prim=len(new_exponents), orbital_indices=new_orbital_indices, - exponents=jnp.array(new_exponents, dtype=dtype), - coefficients=jnp.array(new_coefficients, dtype=dtype), + exponents=np.array(new_exponents, dtype=np.float64), + coefficients=np.array(new_coefficients, dtype=np.float64), angular_momentums=new_angular_momentums, polynominal_order_x=new_polynominal_order_x, polynominal_order_y=new_polynominal_order_y, @@ -1519,8 +1533,7 @@ def _aos_cart_to_sphe(aos_data: AOs_cart_data | AOs_sphe_data) -> tuple[AOs_sphe tuple: (AOs_sphe_data, transform_pinv) where transform_pinv maps Cartesian -> spherical coefficients with shape (num_ao_cart, num_ao_sph). """ - dtype = get_dtype("orb_eval") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("ao_eval") if isinstance(aos_data, AOs_sphe_data): transform_pinv = np.eye(aos_data.num_ao, dtype=dtype_np) return aos_data, transform_pinv @@ -1616,8 +1629,8 @@ def _match_shell(existing: dict, nucleus: int, l: int, exps: np.ndarray, coefs: num_ao=total_sph, num_ao_prim=len(new_exponents), orbital_indices=new_orbital_indices, - exponents=jnp.array(new_exponents, dtype=dtype), - coefficients=jnp.array(new_coefficients, dtype=dtype), + exponents=np.array(new_exponents, dtype=np.float64), + coefficients=np.array(new_coefficients, dtype=np.float64), angular_momentums=new_angular_momentums, magnetic_quantum_numbers=new_magnetic_quantum_numbers, ) @@ -1668,8 +1681,7 @@ def _compute_overlap_1d_cart( def _compute_overlap_matrix_cart_analytic(aos_cart_data: AOs_cart_data) -> npt.NDArray[np.float64]: """Compute AO overlap matrix analytically for Cartesian contracted GTOs.""" - dtype = get_dtype("orb_eval") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("ao_eval") num_ao = aos_cart_data.num_ao overlap_matrix = np.zeros((num_ao, num_ao), dtype=dtype_np) @@ -1740,8 +1752,7 @@ def _estimate_overlap_integration_box( tail_tolerance: float = 1.0e-11, ) -> tuple[np.ndarray, np.ndarray]: """Estimate finite integration bounds for numerical overlap integration.""" - dtype = get_dtype("orb_eval") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("ao_eval") if tail_tolerance <= 0.0 or tail_tolerance >= 1.0: raise ValueError(f"tail_tolerance must satisfy 0 < tail_tolerance < 1. Got {tail_tolerance}.") @@ -1766,8 +1777,7 @@ def _build_overlap_integration_grid( tail_tolerance: float, ) -> tuple[np.ndarray, float]: """Build a uniform midpoint grid and volume element for numerical overlap integration.""" - dtype = get_dtype("orb_eval") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("ao_eval") if num_grid_points < 3: raise ValueError(f"num_grid_points must be >= 3. Got {num_grid_points}.") @@ -1792,8 +1802,7 @@ def _compute_overlap_matrix_debug( tail_tolerance: float = 1.0e-11, ) -> npt.NDArray[np.float64]: """Numerically compute AO overlap matrix by 3D midpoint integration (debug).""" - dtype = get_dtype("orb_eval") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("ao_eval") r_carts, volume_element = _build_overlap_integration_grid( aos_data=aos_data, num_grid_points=num_grid_points, @@ -1812,7 +1821,7 @@ def compute_overlap_matrix(aos_data: AOs_sphe_data | AOs_cart_data) -> jax.Array For spherical AOs, the overlap is evaluated by conversion to Cartesian AOs and transformed back with the spherical-to-Cartesian matrix. """ - dtype = get_dtype("orb_eval") + dtype_jnp = get_dtype_jnp("ao_eval") aos_cart_data, transform_matrix = _aos_sphe_to_cart(aos_data) cart_overlap_matrix = _compute_overlap_matrix_cart_analytic(aos_cart_data) @@ -1824,7 +1833,7 @@ def compute_overlap_matrix(aos_data: AOs_sphe_data | AOs_cart_data) -> jax.Array raise NotImplementedError overlap_matrix = 0.5 * (overlap_matrix + overlap_matrix.T) - return jnp.asarray(overlap_matrix, dtype=dtype) + return jnp.asarray(overlap_matrix, dtype=dtype_jnp) def compute_AOs(aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax.Array) -> jax.Array: @@ -1845,9 +1854,11 @@ def compute_AOs(aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax.Array) -> Raises: NotImplementedError: If ``aos_data`` is neither Cartesian nor spherical. """ - dtype = get_dtype("orb_eval") - r_carts = jnp.asarray(r_carts, dtype=dtype) - + # NOTE: do not pre-cast r_carts here. The internal kernels + # ``_compute_AOs_sphe`` / ``_compute_AOs_cart`` reconstruct ``r - R`` in + # float64 to avoid catastrophic cancellation (positions can be ~50 Bohr; + # an fp32 difference loses ~6 digits). Downcasting r_carts in this wrapper + # would destroy that precision *before* the fp64 reconstruction can use it. if isinstance(aos_data, AOs_sphe_data): AOs = _compute_AOs_sphe(aos_data, r_carts) @@ -1876,8 +1887,6 @@ def _compute_AOs_sphe_debug(aos_data: AOs_sphe_data, r_carts: npt.NDArray[np.flo The method is for computing the value of the given atomic orbital at r_carts for debugging purpose. See compute_AOs_api. """ - dtype = get_dtype("orb_eval") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 aos_values = [] for ao_index in range(aos_data.num_ao): @@ -1931,8 +1940,6 @@ def _compute_AOs_cart_debug(aos_data: AOs_cart_data, r_carts: npt.NDArray[np.flo The method is for computing the value of the given atomic orbital at r_carts for debugging purpose. See compute_AOs_api. """ - dtype = get_dtype("orb_eval") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 aos_values = [] for ao_index in range(aos_data.num_ao): @@ -1986,30 +1993,29 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A See compute_AOs_api """ - # Downcast all float inputs to orb_eval zone dtype (P0-1, P0-2) - dtype = get_dtype("orb_eval") - # Compute r-R in float64 to avoid catastrophic cancellation when zone dtype is float32 - # (positions can be ~50 Bohr; float32 difference loses ~6 digits of precision). - _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) - _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) - r_R_diffs = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) - r_carts = _r_carts_f64.astype(dtype) - R_carts_jnp = _R_carts_f64.astype(dtype) - c_jnp = aos_data._coefficients_jnp.astype(dtype) - Z_jnp = aos_data._exponents_jnp.astype(dtype) + dtype_jnp = get_dtype_jnp("ao_eval") + # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) + # via JAX promotion when one operand is fp64, then downcast to the ao_eval + # zone (Principle 3b — local cast at point of arithmetic). r_carts is + # forwarded as-is (Principle 3a) and R_carts is read from the fp64 storage + # accessor on the basis-data dataclass. + R_carts = aos_data._atomic_center_carts_prim_jnp + r_R_diffs = (r_carts[None, :, :] - R_carts[:, None, :]).astype(dtype_jnp) + c_jnp = aos_data._coefficients_jnp.astype(dtype_jnp) + Z_jnp = aos_data._exponents_jnp.astype(dtype_jnp) l_jnp = aos_data._angular_momentums_prim_jnp nx_jnp = aos_data._polynominal_order_x_prim_jnp ny_jnp = aos_data._polynominal_order_y_prim_jnp nz_jnp = aos_data._polynominal_order_z_prim_jnp - N_n_dup_fuctorial_part = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype) + N_n_dup_fuctorial_part = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) N_n_dup_Z_part = (2.0 * Z_jnp / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z_jnp) ** l_jnp N_n_dup = jnp.sqrt(N_n_dup_Z_part * N_n_dup_fuctorial_part) r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) x, y, z = r_R_diffs[..., 0], r_R_diffs[..., 1], r_R_diffs[..., 2] - eps = get_eps("stabilizing_ao", dtype) + eps = get_eps("stabilizing_ao", dtype_jnp) P_l_nx_ny_nz_dup = (x + eps) ** (nx_jnp[:, None]) * (y + eps) ** (ny_jnp[:, None]) * (z + eps) ** (nz_jnp[:, None]) """ @@ -2039,33 +2045,32 @@ def _compute_AOs_sphe(aos_data: AOs_sphe_data, r_carts: jnpt.ArrayLike) -> jax.A See compute_AOs_api """ - # Downcast all float inputs to orb_eval zone dtype (P0-1) - dtype = get_dtype("orb_eval") - # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. - _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) - _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) - _R_carts_unique_f64 = jnp.asarray(aos_data._atomic_center_carts_unique_jnp, dtype=jnp.float64) - r_R_diffs = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) - r_R_diffs_uq = (_r_carts_f64[None, :, :] - _R_carts_unique_f64[:, None, :]).astype(dtype) - r_carts = _r_carts_f64.astype(dtype) + dtype_jnp = get_dtype_jnp("ao_eval") + # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) + # via JAX promotion when one operand is fp64, then downcast to the ao_eval + # zone (Principle 3b — local cast at point of arithmetic). r_carts is + # forwarded as-is (Principle 3a) and R_carts is read from the fp64 storage + # accessor on the basis-data dataclass. + R_carts = aos_data._atomic_center_carts_prim_jnp + R_carts_unique = aos_data._atomic_center_carts_unique_jnp + r_R_diffs = (r_carts[None, :, :] - R_carts[:, None, :]).astype(dtype_jnp) + r_R_diffs_uq = (r_carts[None, :, :] - R_carts_unique[:, None, :]).astype(dtype_jnp) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp - R_carts_jnp = _R_carts_f64.astype(dtype) - R_carts_unique_jnp = _R_carts_unique_f64.astype(dtype) - c_jnp = aos_data._coefficients_jnp.astype(dtype) - Z_jnp = aos_data._exponents_jnp.astype(dtype) + c_jnp = aos_data._coefficients_jnp.astype(dtype_jnp) + Z_jnp = aos_data._exponents_jnp.astype(dtype_jnp) l_jnp = aos_data._angular_momentums_prim_jnp m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp # Normalization constants computed in zone dtype. - l_typed = l_jnp.astype(dtype) + l_typed = l_jnp.astype(dtype_jnp) factorial_l_plus_1 = jnp.exp(jscipy.special.gammaln(l_typed + 2.0)) factorial_2l_plus_2 = jnp.exp(jscipy.special.gammaln(2.0 * l_typed + 3.0)) N_n_dup = jnp.sqrt( (2.0 ** (2 * l_typed + 3) * factorial_l_plus_1 * (2 * Z_jnp) ** (l_typed + 1.5)) - / (factorial_2l_plus_2 * jnp.sqrt(jnp.asarray(jnp.pi, dtype=dtype))) + / (factorial_2l_plus_2 * jnp.sqrt(jnp.asarray(jnp.pi, dtype=dtype_jnp))) ) - N_l_m_dup = jnp.sqrt((2 * l_typed + 1) / (4 * jnp.asarray(jnp.pi, dtype=dtype))) + N_l_m_dup = jnp.sqrt((2 * l_typed + 1) / (4 * jnp.asarray(jnp.pi, dtype=dtype_jnp))) r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) @@ -2328,40 +2333,40 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j tuple: (values, grads, laps) where values has shape (49, num_R, num_r), grads has shape (49, num_R, num_r, 3), and laps has shape (49, num_R, num_r). """ - dtype = get_dtype("kinetic") + dtype_jnp = get_dtype_jnp("ao_grad_lap") S_L_M_COEFFS = ( - jnp.array([1.0], dtype=dtype), - jnp.array([1.0], dtype=dtype), - jnp.array([1.0], dtype=dtype), - jnp.array([1.0], dtype=dtype), - jnp.array([1.7320508075688774], dtype=dtype), - jnp.array([1.7320508075688774], dtype=dtype), - jnp.array([1.0, -0.5, -0.5], dtype=dtype), - jnp.array([1.7320508075688774], dtype=dtype), - jnp.array([-0.8660254037844387, 0.8660254037844387], dtype=dtype), - jnp.array([-0.7905694150420949, 2.3717082451262845], dtype=dtype), - jnp.array([3.8729833462074166], dtype=dtype), - jnp.array([2.4494897427831783, -0.6123724356957946, -0.6123724356957946], dtype=dtype), - jnp.array([1.0, -1.5, -1.5], dtype=dtype), - jnp.array([2.4494897427831783, -0.6123724356957946, -0.6123724356957946], dtype=dtype), - jnp.array([-1.9364916731037083, 1.9364916731037083], dtype=dtype), - jnp.array([-2.3717082451262845, 0.7905694150420949], dtype=dtype), - jnp.array([-2.958039891549808, 2.958039891549808], dtype=dtype), - jnp.array([-2.091650066335189, 6.274950199005566], dtype=dtype), - jnp.array([6.708203932499371, -1.1180339887498951, -1.1180339887498951], dtype=dtype), - jnp.array([3.1622776601683795, -2.3717082451262845, -2.3717082451262845], dtype=dtype), - jnp.array([1.0, -3.0, 0.375, -3.0, 0.75, 0.375], dtype=dtype), - jnp.array([3.1622776601683795, -2.3717082451262845, -2.3717082451262845], dtype=dtype), - jnp.array([-3.3541019662496856, 0.5590169943749476, 3.3541019662496856, -0.5590169943749476], dtype=dtype), - jnp.array([-6.274950199005566, 2.091650066335189], dtype=dtype), - jnp.array([0.739509972887452, -4.437059837324712, 0.739509972887452], dtype=dtype), - jnp.array([0.7015607600201141, -7.015607600201141, 3.5078038001005707], dtype=dtype), - jnp.array([-8.874119674649426, 8.874119674649426], dtype=dtype), + jnp.array([1.0], dtype=dtype_jnp), + jnp.array([1.0], dtype=dtype_jnp), + jnp.array([1.0], dtype=dtype_jnp), + jnp.array([1.0], dtype=dtype_jnp), + jnp.array([1.7320508075688774], dtype=dtype_jnp), + jnp.array([1.7320508075688774], dtype=dtype_jnp), + jnp.array([1.0, -0.5, -0.5], dtype=dtype_jnp), + jnp.array([1.7320508075688774], dtype=dtype_jnp), + jnp.array([-0.8660254037844387, 0.8660254037844387], dtype=dtype_jnp), + jnp.array([-0.7905694150420949, 2.3717082451262845], dtype=dtype_jnp), + jnp.array([3.8729833462074166], dtype=dtype_jnp), + jnp.array([2.4494897427831783, -0.6123724356957946, -0.6123724356957946], dtype=dtype_jnp), + jnp.array([1.0, -1.5, -1.5], dtype=dtype_jnp), + jnp.array([2.4494897427831783, -0.6123724356957946, -0.6123724356957946], dtype=dtype_jnp), + jnp.array([-1.9364916731037083, 1.9364916731037083], dtype=dtype_jnp), + jnp.array([-2.3717082451262845, 0.7905694150420949], dtype=dtype_jnp), + jnp.array([-2.958039891549808, 2.958039891549808], dtype=dtype_jnp), + jnp.array([-2.091650066335189, 6.274950199005566], dtype=dtype_jnp), + jnp.array([6.708203932499371, -1.1180339887498951, -1.1180339887498951], dtype=dtype_jnp), + jnp.array([3.1622776601683795, -2.3717082451262845, -2.3717082451262845], dtype=dtype_jnp), + jnp.array([1.0, -3.0, 0.375, -3.0, 0.75, 0.375], dtype=dtype_jnp), + jnp.array([3.1622776601683795, -2.3717082451262845, -2.3717082451262845], dtype=dtype_jnp), + jnp.array([-3.3541019662496856, 0.5590169943749476, 3.3541019662496856, -0.5590169943749476], dtype=dtype_jnp), + jnp.array([-6.274950199005566, 2.091650066335189], dtype=dtype_jnp), + jnp.array([0.739509972887452, -4.437059837324712, 0.739509972887452], dtype=dtype_jnp), + jnp.array([0.7015607600201141, -7.015607600201141, 3.5078038001005707], dtype=dtype_jnp), + jnp.array([-8.874119674649426, 8.874119674649426], dtype=dtype_jnp), jnp.array( [-4.183300132670378, 0.5229125165837972, 12.549900398011133, -1.0458250331675945, -1.5687375497513916], - dtype=dtype, + dtype=dtype_jnp, ), - jnp.array([10.2469507659596, -5.1234753829798, -5.1234753829798], dtype=dtype), + jnp.array([10.2469507659596, -5.1234753829798, -5.1234753829798], dtype=dtype_jnp), jnp.array( [ 3.872983346207417, @@ -2371,9 +2376,9 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j 0.9682458365518543, 0.4841229182759271, ], - dtype=dtype, + dtype=dtype_jnp, ), - jnp.array([1.0, -5.0, 1.875, -5.0, 3.75, 1.875], dtype=dtype), + jnp.array([1.0, -5.0, 1.875, -5.0, 3.75, 1.875], dtype=dtype_jnp), jnp.array( [ 3.872983346207417, @@ -2383,21 +2388,21 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j 0.9682458365518543, 0.4841229182759271, ], - dtype=dtype, + dtype=dtype_jnp, ), - jnp.array([-5.1234753829798, 2.5617376914899, 5.1234753829798, -2.5617376914899], dtype=dtype), + jnp.array([-5.1234753829798, 2.5617376914899, 5.1234753829798, -2.5617376914899], dtype=dtype_jnp), jnp.array( [-12.549900398011133, 1.5687375497513916, 4.183300132670378, 1.0458250331675945, -0.5229125165837972], - dtype=dtype, + dtype=dtype_jnp, ), - jnp.array([2.2185299186623566, -13.311179511974139, 2.2185299186623566], dtype=dtype), - jnp.array([3.5078038001005707, -7.015607600201141, 0.7015607600201141], dtype=dtype), - jnp.array([4.030159736288377, -13.433865787627923, 4.030159736288377], dtype=dtype), - jnp.array([2.3268138086232857, -23.268138086232856, 11.634069043116428], dtype=dtype), - jnp.array([-19.843134832984433, 1.9843134832984433, 19.843134832984433, -1.9843134832984433], dtype=dtype), + jnp.array([2.2185299186623566, -13.311179511974139, 2.2185299186623566], dtype=dtype_jnp), + jnp.array([3.5078038001005707, -7.015607600201141, 0.7015607600201141], dtype=dtype_jnp), + jnp.array([4.030159736288377, -13.433865787627923, 4.030159736288377], dtype=dtype_jnp), + jnp.array([2.3268138086232857, -23.268138086232856, 11.634069043116428], dtype=dtype_jnp), + jnp.array([-19.843134832984433, 1.9843134832984433, 19.843134832984433, -1.9843134832984433], dtype=dtype_jnp), jnp.array( [-7.245688373094719, 2.7171331399105196, 21.737065119284157, -5.434266279821039, -8.15139941973156], - dtype=dtype, + dtype=dtype_jnp, ), jnp.array( [ @@ -2408,16 +2413,16 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j 1.8114220932736798, 0.9057110466368399, ], - dtype=dtype, + dtype=dtype_jnp, ), jnp.array( [4.58257569495584, -11.4564392373896, 2.8641098093474, -11.4564392373896, 5.7282196186948, 2.8641098093474], - dtype=dtype, + dtype=dtype_jnp, ), - jnp.array([1.0, -7.5, 5.625, -0.3125, -7.5, 11.25, -0.9375, 5.625, -0.9375, -0.3125], dtype=dtype), + jnp.array([1.0, -7.5, 5.625, -0.3125, -7.5, 11.25, -0.9375, 5.625, -0.9375, -0.3125], dtype=dtype_jnp), jnp.array( [4.58257569495584, -11.4564392373896, 2.8641098093474, -11.4564392373896, 5.7282196186948, 2.8641098093474], - dtype=dtype, + dtype=dtype_jnp, ), jnp.array( [ @@ -2430,11 +2435,11 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j 0.45285552331841994, 0.45285552331841994, ], - dtype=dtype, + dtype=dtype_jnp, ), jnp.array( [-21.737065119284157, 8.15139941973156, 7.245688373094719, 5.434266279821039, -2.7171331399105196], - dtype=dtype, + dtype=dtype_jnp, ), jnp.array( [ @@ -2446,10 +2451,10 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j 2.480391854123054, -0.4960783708246108, ], - dtype=dtype, + dtype=dtype_jnp, ), - jnp.array([11.634069043116428, -23.268138086232856, 2.3268138086232857], dtype=dtype), - jnp.array([-0.6716932893813962, 10.075399340720942, -10.075399340720942, 0.6716932893813962], dtype=dtype), + jnp.array([11.634069043116428, -23.268138086232856, 2.3268138086232857], dtype=dtype_jnp), + jnp.array([-0.6716932893813962, 10.075399340720942, -10.075399340720942, 0.6716932893813962], dtype=dtype_jnp), ) S_L_M_EXPS = ( @@ -2575,26 +2580,26 @@ def _single_val_grad_lap(diff: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.A @jit def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for Cartesian AOs (contracted).""" - dtype = get_dtype("kinetic") - # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. - _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) - _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) - diff = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) - r_carts = _r_carts_f64.astype(dtype) - R_carts = _R_carts_f64.astype(dtype) - c = aos_data._coefficients_jnp.astype(dtype) - Z = aos_data._exponents_jnp.astype(dtype) + dtype_jnp = get_dtype_jnp("ao_grad_lap") + # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) + # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). + # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage + # accessor on the basis-data dataclass. + R_carts = aos_data._atomic_center_carts_prim_jnp + diff = (r_carts[None, :, :] - R_carts[:, None, :]).astype(dtype_jnp) + c = aos_data._coefficients_jnp.astype(dtype_jnp) + Z = aos_data._exponents_jnp.astype(dtype_jnp) l = aos_data._angular_momentums_prim_jnp nx = aos_data._polynominal_order_x_prim_jnp ny = aos_data._polynominal_order_y_prim_jnp nz = aos_data._polynominal_order_z_prim_jnp - N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype) + N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z) ** l N = jnp.sqrt(N_Z * N_fact) x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] - eps = get_eps("stabilizing_ao", dtype) + eps = get_eps("stabilizing_ao", dtype_jnp) x = x + eps y = y + eps z = z + eps @@ -2624,24 +2629,23 @@ def _second_component(base, n): @jit def _compute_AOs_laplacian_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for spherical AOs (contracted).""" - dtype = get_dtype("kinetic") - # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. - _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) - _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) - _R_carts_unique_f64 = jnp.asarray(aos_data._atomic_center_carts_unique_jnp, dtype=jnp.float64) - r_R_diffs = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) - r_R_diffs_uq = (_r_carts_f64[None, :, :] - _R_carts_unique_f64[:, None, :]).astype(dtype) - r_carts = _r_carts_f64.astype(dtype) + dtype_jnp = get_dtype_jnp("ao_grad_lap") + # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) + # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). + # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage + # accessor on the basis-data dataclass. + R_carts = aos_data._atomic_center_carts_prim_jnp + R_carts_unique = aos_data._atomic_center_carts_unique_jnp + r_R_diffs = (r_carts[None, :, :] - R_carts[:, None, :]).astype(dtype_jnp) + r_R_diffs_uq = (r_carts[None, :, :] - R_carts_unique[:, None, :]).astype(dtype_jnp) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp - R_carts_jnp = _R_carts_f64.astype(dtype) - R_carts_unique_jnp = _R_carts_unique_f64.astype(dtype) - c_jnp = aos_data._coefficients_jnp.astype(dtype) - Z_jnp = aos_data._exponents_jnp.astype(dtype) + c_jnp = aos_data._coefficients_jnp.astype(dtype_jnp) + Z_jnp = aos_data._exponents_jnp.astype(dtype_jnp) l_jnp = aos_data._angular_momentums_prim_jnp m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp - l_f64 = l_jnp.astype(dtype) - Z_f64 = Z_jnp.astype(dtype) + l_f64 = l_jnp.astype(dtype_jnp) + Z_f64 = Z_jnp.astype(dtype_jnp) factorial_l_plus_1 = jnp.exp(jscipy.special.gammaln(l_f64 + 2.0)) factorial_2l_plus_2 = jnp.exp(jscipy.special.gammaln(2.0 * l_f64 + 3.0)) @@ -2700,9 +2704,9 @@ def compute_AOs_laplacian(aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax. Raises: NotImplementedError: If ``aos_data`` is not Cartesian or spherical. """ - dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts, dtype=dtype) - + # NOTE: do not pre-cast r_carts here. The analytic kernels reconstruct + # ``r - R`` in float64 internally to avoid catastrophic cancellation; a + # premature downcast in this wrapper would defeat that guard. if isinstance(aos_data, AOs_cart_data): return _compute_AOs_laplacian_analytic_cart(aos_data, r_carts) @@ -2755,8 +2759,7 @@ def _compute_S_l_m_debug( They can be hardcoded into a code, or they can be computed analytically (e.g., https://en.wikipedia.org/wiki/Solid_harmonics). The latter one is the strategy employed in this code, """ - dtype = get_dtype("orb_eval") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("ao_eval") R_cart = atomic_center_cart x, y, z = np.array(r_cart, dtype=dtype_np) - np.array(R_cart, dtype=dtype_np) r_norm = LA.norm(np.array(r_cart, dtype=dtype_np) - np.array(R_cart, dtype=dtype_np)) @@ -2813,8 +2816,9 @@ def _compute_AOs_laplacian_autodiff(aos_data: AOs_sphe_data | AOs_cart_data, r_c See compute_AOs_laplacian_api """ - dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts, dtype=dtype) + # Forward r_carts as-is (Principle 3a — no parameter rebind). compute_AOs's + # inner kernels reconstruct r-R in caller-supplied precision and downcast to + # the ao_eval zone at the use site; the hessian inherits that dtype. # not very fast, but it works. ao_matrix_hessian = hessian(compute_AOs, argnums=1)(aos_data, r_carts) ao_matrix_laplacian = jnp.einsum("m i i u i u -> mi", ao_matrix_hessian) @@ -2840,8 +2844,6 @@ def _compute_AOs_laplacian_debug( Array containing laplacians of the AOs at r_carts. The dim. is (num_ao, N_e) """ - dtype = get_dtype("kinetic") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 # noqa: F841 # Laplacians of AOs (numerical) diff_h = 1.0e-5 @@ -2890,26 +2892,26 @@ def _compute_AOs_laplacian_debug( @jit def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for Cartesian AOs (contracted).""" - dtype = get_dtype("kinetic") - # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. - _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) - _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) - diff = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) - r_carts = _r_carts_f64.astype(dtype) - R_carts = _R_carts_f64.astype(dtype) - c = aos_data._coefficients_jnp.astype(dtype) - Z = aos_data._exponents_jnp.astype(dtype) + dtype_jnp = get_dtype_jnp("ao_grad_lap") + # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) + # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). + # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage + # accessor on the basis-data dataclass. + R_carts = aos_data._atomic_center_carts_prim_jnp + diff = (r_carts[None, :, :] - R_carts[:, None, :]).astype(dtype_jnp) + c = aos_data._coefficients_jnp.astype(dtype_jnp) + Z = aos_data._exponents_jnp.astype(dtype_jnp) l = aos_data._angular_momentums_prim_jnp nx = aos_data._polynominal_order_x_prim_jnp ny = aos_data._polynominal_order_y_prim_jnp nz = aos_data._polynominal_order_z_prim_jnp - N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype) + N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z) ** l N = jnp.sqrt(N_Z * N_fact) x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] - eps = get_eps("stabilizing_ao", dtype) + eps = get_eps("stabilizing_ao", dtype_jnp) x = x + eps y = y + eps z = z + eps @@ -2942,24 +2944,23 @@ def _grad_component(base, n): @jit def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for spherical AOs (contracted).""" - dtype = get_dtype("kinetic") - # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. - _r_carts_f64 = jnp.asarray(r_carts, dtype=jnp.float64) - _R_carts_f64 = jnp.asarray(aos_data._atomic_center_carts_prim_jnp, dtype=jnp.float64) - _R_carts_unique_f64 = jnp.asarray(aos_data._atomic_center_carts_unique_jnp, dtype=jnp.float64) - r_R_diffs = (_r_carts_f64[None, :, :] - _R_carts_f64[:, None, :]).astype(dtype) - r_R_diffs_uq = (_r_carts_f64[None, :, :] - _R_carts_unique_f64[:, None, :]).astype(dtype) - r_carts = _r_carts_f64.astype(dtype) + dtype_jnp = get_dtype_jnp("ao_grad_lap") + # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) + # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). + # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage + # accessor on the basis-data dataclass. + R_carts = aos_data._atomic_center_carts_prim_jnp + R_carts_unique = aos_data._atomic_center_carts_unique_jnp + r_R_diffs = (r_carts[None, :, :] - R_carts[:, None, :]).astype(dtype_jnp) + r_R_diffs_uq = (r_carts[None, :, :] - R_carts_unique[:, None, :]).astype(dtype_jnp) nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp - R_carts_jnp = _R_carts_f64.astype(dtype) - R_carts_unique_jnp = _R_carts_unique_f64.astype(dtype) - c_jnp = aos_data._coefficients_jnp.astype(dtype) - Z_jnp = aos_data._exponents_jnp.astype(dtype) + c_jnp = aos_data._coefficients_jnp.astype(dtype_jnp) + Z_jnp = aos_data._exponents_jnp.astype(dtype_jnp) l_jnp = aos_data._angular_momentums_prim_jnp m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp - l_f64 = l_jnp.astype(dtype) - Z_f64 = Z_jnp.astype(dtype) + l_f64 = l_jnp.astype(dtype_jnp) + Z_f64 = Z_jnp.astype(dtype_jnp) factorial_l_plus_1 = jnp.exp(jscipy.special.gammaln(l_f64 + 2.0)) factorial_2l_plus_2 = jnp.exp(jscipy.special.gammaln(2.0 * l_f64 + 3.0)) @@ -3026,9 +3027,9 @@ def compute_AOs_grad(aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax.Array Raises: NotImplementedError: If ``aos_data`` is neither Cartesian nor spherical. """ - dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts, dtype=dtype) - + # NOTE: do not pre-cast r_carts here. The analytic kernels reconstruct + # ``r - R`` in float64 internally to avoid catastrophic cancellation; a + # premature downcast in this wrapper would defeat that guard. if isinstance(aos_data, AOs_cart_data): return _compute_AOs_grad_analytic_cart(aos_data, r_carts) @@ -3055,7 +3056,6 @@ def _compute_AOs_grad_autodiff( The dim. of each matrix is (num_ao, N_e) """ - dtype = get_dtype("kinetic") # noqa: F841 grad_full = jacrev(compute_AOs, argnums=1)(aos_data, r_carts) grad_diag = jnp.diagonal(grad_full, axis1=1, axis2=2) grad_diag = jnp.swapaxes(grad_diag, 1, 2) @@ -3077,8 +3077,6 @@ def _compute_AOs_grad_debug( the given atomic orbital at r_carts using FDM for debugging JAX implementations. See compute_AOs_grad_api """ - dtype = get_dtype("kinetic") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 # noqa: F841 # Gradients of AOs (numerical) diff_h = 1.0e-5 diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 1968935f..e81006c8 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -61,15 +61,15 @@ from jax.scipy import linalg as jsp_linalg from scipy.special import eval_legendre +from ._function_collections import _legendre_tablated as jnp_legendre_tablated +from ._precision import get_dtype_jnp, get_dtype_np +from ._setting import NN_default, Nv_default from .determinant import ( _compute_ratio_determinant_part_split_spin, compute_det_geminal_all_elements, compute_geminal_all_elements, ) -from ._function_collections import _legendre_tablated as jnp_legendre_tablated from .jastrow_factor import _compute_ratio_Jastrow_part_split_spin, compute_Jastrow_part -from ._precision import get_dtype -from ._setting import NN_default, Nv_default from .structure import ( Structure_data, _find_nearest_nucleus_indices_jnp, @@ -612,10 +612,9 @@ def _compute_ecp_local_parts_all_pairs_debug( Returns: float: The sum of local part of the given ECPs with r_up_carts and r_dn_carts. """ - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) - r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) + # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). The + # accumulated scalar V_local is cast to the coulomb zone before return. + dtype_np = get_dtype_np("coulomb") V_local = 0.0 for i_atom in range(coulomb_potential_data.structure_data.natom): @@ -654,7 +653,8 @@ def _compute_ecp_local_parts_all_pairs_debug( for a, n, b in zip(coefficients, powers, exponents, strict=True) ] ) - return V_local + # Cast accumulator to coulomb zone (Principle 3b). + return np.asarray(V_local, dtype=dtype_np) def _compute_ecp_non_local_parts_all_pairs_debug( @@ -686,11 +686,9 @@ def _compute_ecp_non_local_parts_all_pairs_debug( list[float]: The list of non-local part of the given ECPs with r_up_carts and r_dn_carts. float: sum of the V_nonlocal """ - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) - r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) - RT = np.asarray(RT, dtype=dtype_np) + # Forward r_up/dn_carts/RT as-is (Principle 3a — no parameter rebind). + # Cast RT to coulomb zone at the use site (the grid_points rotation below). + dtype_np = get_dtype_np("coulomb") # noqa: F841 if Nv == 4: weights = tetrahedron_sym_mesh_Nv4.weights @@ -707,7 +705,8 @@ def _compute_ecp_non_local_parts_all_pairs_debug( else: raise NotImplementedError - grid_points = grid_points @ RT # rotate the grid points. dim. (N,3) @ (3,3) = (N,3) + # Cast RT to coulomb zone at the use site (Principle 3b). + grid_points = grid_points @ np.asarray(RT, dtype=dtype_np) # rotate the grid points. dim. (N,3) @ (3,3) = (N,3) mesh_non_local_ecp_part = [] V_nonlocal = [] @@ -875,11 +874,9 @@ def _compute_ecp_non_local_parts_nearest_neighbors_debug( list[float]: The list of non-local part of the given ECPs with r_up_carts and r_dn_carts. float: sum of the V_nonlocal """ - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) - r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) - RT = np.asarray(RT, dtype=dtype_np) + # Forward r_up/dn_carts/RT as-is (Principle 3a — no parameter rebind). + # Cast RT to coulomb zone at the use site (the grid_points rotation below). + dtype_np = get_dtype_np("coulomb") # noqa: F841 if Nv == 4: weights = tetrahedron_sym_mesh_Nv4.weights @@ -896,7 +893,8 @@ def _compute_ecp_non_local_parts_nearest_neighbors_debug( else: raise NotImplementedError - grid_points = grid_points @ RT # rotate the grid points. dim. (N,3) @ (3,3) = (N,3) + # Cast RT to coulomb zone at the use site (Principle 3b). + grid_points = grid_points @ np.asarray(RT, dtype=dtype_np) # rotate the grid points. dim. (N,3) @ (3,3) = (N,3) V_nonlocal = [] sum_V_nonlocal = 0.0 @@ -1117,11 +1115,7 @@ def _compute_ecp_coulomb_potential_debug( Returns: float: The sum of non-local part of the given ECPs with r_up_carts and r_dn_carts. """ - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) - r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) - RT = np.asarray(RT, dtype=dtype_np) + # Forward r_up/dn_carts/RT as-is (Principle 3a — no parameter rebind). ecp_local_parts = _compute_ecp_local_parts_all_pairs_debug( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts @@ -1167,7 +1161,7 @@ def compute_V_l(r_cart, i_atom, exponent, coefficient, power): structure_data=coulomb_potential_data.structure_data, r_cart=r_cart, i_atom=i_atom, - dtype=dtype, + dtype=dtype_jnp, ) V_l = ( jnp.linalg.norm(rel_R_cart_min_dist) ** -2.0 @@ -1210,11 +1204,14 @@ def compute_V_local( ) # Vectrized (flatten) arguments are prepared here. - dtype = get_dtype("coulomb") - r_up_carts_jnp = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts_jnp = jnp.asarray(r_dn_carts, dtype=dtype) - - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts here. _get_min_dist_rel_R_cart_jnp + # (called inside compute_V_l) reconstructs R - r in fp64 internally; pre-casting to + # fp32 here would destroy precision before that reconstruction. + dtype_jnp = get_dtype_jnp("coulomb") + r_up_carts_jnp = r_up_carts + r_dn_carts_jnp = r_dn_carts + + dtype_np = get_dtype_np("coulomb") i_atom_np = np.array(coulomb_potential_data._nucleus_index_local_part) exponent_np = np.array(coulomb_potential_data._exponents_local_part, dtype=dtype_np) coefficient_np = np.array(coulomb_potential_data._coefficients_local_part, dtype=dtype_np) @@ -1275,27 +1272,29 @@ def compute_ecp_non_local_parts_nearest_neighbors( - Non-local ECP contributions per configuration (flattened). - Scalar sum of all non-local contributions. """ - dtype = get_dtype("coulomb") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - RT = jnp.asarray(RT, dtype=dtype) + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts here. They are forwarded to + # compute_Jastrow_part / compute_det_geminal_all_elements (fp64 zones); pre-casting + # to the coulomb (fp32) zone would silently down-cast inputs to those fp64 zones. + # RT is also forwarded as-is (Principle 3a); cast at the use site below. + dtype_jnp = get_dtype_jnp("coulomb") if Nv == 4: - weights = jnp.array(tetrahedron_sym_mesh_Nv4.weights, dtype=dtype) - grid_points = jnp.array(tetrahedron_sym_mesh_Nv4.grid_points, dtype=dtype) + weights = jnp.array(tetrahedron_sym_mesh_Nv4.weights, dtype=dtype_jnp) + grid_points = jnp.array(tetrahedron_sym_mesh_Nv4.grid_points, dtype=dtype_jnp) elif Nv == 6: - weights = jnp.array(octahedron_sym_mesh_Nv6.weights, dtype=dtype) - grid_points = jnp.array(octahedron_sym_mesh_Nv6.grid_points, dtype=dtype) + weights = jnp.array(octahedron_sym_mesh_Nv6.weights, dtype=dtype_jnp) + grid_points = jnp.array(octahedron_sym_mesh_Nv6.grid_points, dtype=dtype_jnp) elif Nv == 12: - weights = jnp.array(icosahedron_sym_mesh_Nv12.weights, dtype=dtype) - grid_points = jnp.array(icosahedron_sym_mesh_Nv12.grid_points, dtype=dtype) + weights = jnp.array(icosahedron_sym_mesh_Nv12.weights, dtype=dtype_jnp) + grid_points = jnp.array(icosahedron_sym_mesh_Nv12.grid_points, dtype=dtype_jnp) elif Nv == 18: - weights = jnp.array(octahedron_sym_mesh_Nv18.weights, dtype=dtype) - grid_points = jnp.array(octahedron_sym_mesh_Nv18.grid_points, dtype=dtype) + weights = jnp.array(octahedron_sym_mesh_Nv18.weights, dtype=dtype_jnp) + grid_points = jnp.array(octahedron_sym_mesh_Nv18.grid_points, dtype=dtype_jnp) else: raise NotImplementedError - grid_points = grid_points @ RT # rotate the grid points. dim. (N,3) @ (3,3) = (N,3) + # Cast RT to coulomb zone at the use site (Principle 3b). + grid_points = grid_points @ RT.astype(dtype_jnp) # rotate the grid points. dim. (N,3) @ (3,3) = (N,3) grid_norm = jnp.linalg.norm(grid_points, axis=1, keepdims=True) # jnp variables @@ -1303,8 +1302,8 @@ def compute_ecp_non_local_parts_nearest_neighbors( global_max_ang_mom_plus_1 = coulomb_potential_data._global_max_ang_mom_plus_1 # stored - non_local_ecp_part_r_carts_up = jnp.zeros((0, len(r_up_carts), 3), dtype=dtype) - non_local_ecp_part_r_carts_dn = jnp.zeros((0, len(r_dn_carts), 3), dtype=dtype) + non_local_ecp_part_r_carts_up = jnp.zeros((0, len(r_up_carts), 3), dtype=dtype_jnp) + non_local_ecp_part_r_carts_dn = jnp.zeros((0, len(r_dn_carts), 3), dtype=dtype_jnp) # cos_theta_all = jnp.zeros((0,)) # weight_all = jnp.zeros((0,)) # V_l_mapped_all = jnp.zeros((global_max_ang_mom_plus_1, 0)) @@ -1330,11 +1329,11 @@ def _build_mesh_for_spin(r_carts, other_carts): n_other = other_carts.shape[0] if n_spin == 0: return ( - jnp.zeros((0, n_spin, 3), dtype=dtype), - jnp.zeros((0, n_other, 3), dtype=dtype), - jnp.zeros((n_spin, 0, global_max_ang_mom_plus_1), dtype=dtype), - jnp.zeros((0,), dtype=dtype), - jnp.zeros((0,), dtype=dtype), + jnp.zeros((0, n_spin, 3), dtype=dtype_jnp), + jnp.zeros((0, n_other, 3), dtype=dtype_jnp), + jnp.zeros((n_spin, 0, global_max_ang_mom_plus_1), dtype=dtype_jnp), + jnp.zeros((0,), dtype=dtype_jnp), + jnp.zeros((0,), dtype=dtype_jnp), ) i_atom_lists = vmap( @@ -1351,7 +1350,7 @@ def _rels_for_electron(r_cart, i_atom_list): structure_data=coulomb_potential_data.structure_data, r_cart=r_cart, i_atom=i_atom, - dtype=dtype, + dtype=dtype_jnp, ) )(i_atom_list) @@ -1366,7 +1365,7 @@ def _rels_for_electron(r_cart, i_atom_list): base = r_carts[None, None, None, :, :] r_carts_on_mesh = base + delta_full # (n_spin, NN, Nv, n_spin, 3) if n_other == 0: - other_carts_on_mesh = jnp.zeros((n_spin, NN, grid_points.shape[0], 0, 3), dtype=dtype) + other_carts_on_mesh = jnp.zeros((n_spin, NN, grid_points.shape[0], 0, 3), dtype=dtype_jnp) else: other_carts_on_mesh = jnp.broadcast_to(other_carts, (n_spin, NN, grid_points.shape[0], n_other, 3)) @@ -1388,7 +1387,7 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): r_mesh = r_carts_on_mesh.reshape(-1, n_spin, 3) if n_other == 0: - other_mesh = jnp.zeros((r_mesh.shape[0], 0, 3), dtype=dtype) + other_mesh = jnp.zeros((r_mesh.shape[0], 0, 3), dtype=dtype_jnp) else: other_mesh = other_carts_on_mesh.reshape(-1, n_other, 3) return r_mesh, other_mesh, V_l_mapped, cos_theta.reshape(-1), weight.reshape(-1) @@ -1417,6 +1416,12 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): wavefunction_data.geminal_data, non_local_ecp_part_r_carts_up, non_local_ecp_part_r_carts_dn ) + # Cast all ratio inputs to the local coulomb zone dtype before arithmetic. + # This follows the consumer-zone rule and avoids any implicit promotion. + jastrow_x = jnp.asarray(jastrow_x, dtype=dtype_jnp) + jastrow_xp = jnp.asarray(jastrow_xp, dtype=dtype_jnp) + det_x = jnp.asarray(det_x, dtype=dtype_jnp) + det_xp = jnp.asarray(det_xp, dtype=dtype_jnp) wf_ratio_all = jnp.exp(jastrow_xp - jastrow_x) * det_xp / det_x # Split ratios for up/dn blocks to avoid big concat of V_l / cos / weight. @@ -1503,27 +1508,27 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( used in the MCMC loop. Passing an inverse from a different configuration silently produces incorrect non-local ECP contributions. """ - dtype = get_dtype("coulomb") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - RT = jnp.asarray(RT, dtype=dtype) + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts here (forwarded to fp64 Jastrow/det zones). + # RT is also forwarded as-is (Principle 3a); cast at the use site below. + dtype_jnp = get_dtype_jnp("coulomb") if Nv == 4: - weights = jnp.array(tetrahedron_sym_mesh_Nv4.weights, dtype=dtype) - grid_points = jnp.array(tetrahedron_sym_mesh_Nv4.grid_points, dtype=dtype) + weights = jnp.array(tetrahedron_sym_mesh_Nv4.weights, dtype=dtype_jnp) + grid_points = jnp.array(tetrahedron_sym_mesh_Nv4.grid_points, dtype=dtype_jnp) elif Nv == 6: - weights = jnp.array(octahedron_sym_mesh_Nv6.weights, dtype=dtype) - grid_points = jnp.array(octahedron_sym_mesh_Nv6.grid_points, dtype=dtype) + weights = jnp.array(octahedron_sym_mesh_Nv6.weights, dtype=dtype_jnp) + grid_points = jnp.array(octahedron_sym_mesh_Nv6.grid_points, dtype=dtype_jnp) elif Nv == 12: - weights = jnp.array(icosahedron_sym_mesh_Nv12.weights, dtype=dtype) - grid_points = jnp.array(icosahedron_sym_mesh_Nv12.grid_points, dtype=dtype) + weights = jnp.array(icosahedron_sym_mesh_Nv12.weights, dtype=dtype_jnp) + grid_points = jnp.array(icosahedron_sym_mesh_Nv12.grid_points, dtype=dtype_jnp) elif Nv == 18: - weights = jnp.array(octahedron_sym_mesh_Nv18.weights, dtype=dtype) - grid_points = jnp.array(octahedron_sym_mesh_Nv18.grid_points, dtype=dtype) + weights = jnp.array(octahedron_sym_mesh_Nv18.weights, dtype=dtype_jnp) + grid_points = jnp.array(octahedron_sym_mesh_Nv18.grid_points, dtype=dtype_jnp) else: raise NotImplementedError - grid_points = grid_points @ RT # rotate the grid points. dim. (N,3) @ (3,3) = (N,3) + # Cast RT to coulomb zone at the use site (Principle 3b). + grid_points = grid_points @ RT.astype(dtype_jnp) # rotate the grid points. dim. (N,3) @ (3,3) = (N,3) grid_norm = jnp.linalg.norm(grid_points, axis=1, keepdims=True) # jnp variables @@ -1531,8 +1536,8 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( global_max_ang_mom_plus_1 = coulomb_potential_data._global_max_ang_mom_plus_1 # stored - non_local_ecp_part_r_carts_up = jnp.zeros((0, len(r_up_carts), 3), dtype=dtype) - non_local_ecp_part_r_carts_dn = jnp.zeros((0, len(r_dn_carts), 3), dtype=dtype) + non_local_ecp_part_r_carts_up = jnp.zeros((0, len(r_up_carts), 3), dtype=dtype_jnp) + non_local_ecp_part_r_carts_dn = jnp.zeros((0, len(r_dn_carts), 3), dtype=dtype_jnp) # cos_theta_all = jnp.zeros((0,)) # weight_all = jnp.zeros((0,)) # V_l_mapped_all = jnp.zeros((global_max_ang_mom_plus_1, 0)) @@ -1558,11 +1563,11 @@ def _build_mesh_for_spin(r_carts, other_carts): n_other = other_carts.shape[0] if n_spin == 0: return ( - jnp.zeros((0, n_spin, 3), dtype=dtype), - jnp.zeros((0, n_other, 3), dtype=dtype), - jnp.zeros((global_max_ang_mom_plus_1, 0), dtype=dtype), - jnp.zeros((0,), dtype=dtype), - jnp.zeros((0,), dtype=dtype), + jnp.zeros((0, n_spin, 3), dtype=dtype_jnp), + jnp.zeros((0, n_other, 3), dtype=dtype_jnp), + jnp.zeros((global_max_ang_mom_plus_1, 0), dtype=dtype_jnp), + jnp.zeros((0,), dtype=dtype_jnp), + jnp.zeros((0,), dtype=dtype_jnp), ) i_atom_lists = vmap( @@ -1579,7 +1584,7 @@ def _rels_for_electron(r_cart, i_atom_list): structure_data=coulomb_potential_data.structure_data, r_cart=r_cart, i_atom=i_atom, - dtype=dtype, + dtype=dtype_jnp, ) )(i_atom_list) @@ -1594,7 +1599,7 @@ def _rels_for_electron(r_cart, i_atom_list): base = r_carts[None, None, None, :, :] r_carts_on_mesh = base + delta_full # (n_spin, NN, Nv, n_spin, 3) if n_other == 0: - other_carts_on_mesh = jnp.zeros((n_spin, NN, grid_points.shape[0], 0, 3), dtype=dtype) + other_carts_on_mesh = jnp.zeros((n_spin, NN, grid_points.shape[0], 0, 3), dtype=dtype_jnp) else: other_carts_on_mesh = jnp.broadcast_to(other_carts, (n_spin, NN, grid_points.shape[0], n_other, 3)) @@ -1618,7 +1623,7 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): r_mesh = r_carts_on_mesh.reshape(-1, n_spin, 3) if n_other == 0: - other_mesh = jnp.zeros((r_mesh.shape[0], 0, 3), dtype=dtype) + other_mesh = jnp.zeros((r_mesh.shape[0], 0, 3), dtype=dtype_jnp) else: other_mesh = other_carts_on_mesh.reshape(-1, n_other, 3) return r_mesh, other_mesh, V_l_all, cos_theta.reshape(-1), weight.reshape(-1) @@ -1640,6 +1645,9 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): new_r_up_shifted=up_mesh_r_up, new_r_dn_shifted=dn_mesh_r_dn, ) + # Cast determinant/Jastrow ratio terms to the local coulomb zone dtype + # before downstream contractions; avoid relying on implicit promotion. + det_ratio = jnp.asarray(det_ratio, dtype=dtype_jnp) if flag_determinant_only: wf_ratio_all = det_ratio else: @@ -1650,6 +1658,7 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): new_r_up_shifted=up_mesh_r_up, new_r_dn_shifted=dn_mesh_r_dn, ) + jastrow_ratio = jnp.asarray(jastrow_ratio, dtype=dtype_jnp) wf_ratio_all = det_ratio * jastrow_ratio # Split ratios for up/dn blocks to avoid big concat of V_l / cos / weight. @@ -1660,7 +1669,7 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): def _contract_chunk(V_l_chunk, cos_chunk, weight_chunk, wf_ratio_chunk): cos_chunk = jnp.array(cos_chunk) weight_chunk = jnp.array(weight_chunk) - wf_ratio_chunk = jnp.array(wf_ratio_chunk) + wf_ratio_chunk = jnp.array(wf_ratio_chunk, dtype=dtype_jnp) P_l_chunk = vmap(vmap(compute_P_l, in_axes=(None, 0, 0, 0)), in_axes=(0, None, None, None))( jnp.arange(global_max_ang_mom_plus_1), cos_chunk, weight_chunk, wf_ratio_chunk ) @@ -1706,10 +1715,9 @@ def compute_ecp_non_local_parts_all_pairs( - Non-local ECP contributions per configuration (flattened). - Scalar sum of all non-local contributions. """ - dtype = get_dtype("coulomb") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - RT = jnp.asarray(RT, dtype=dtype) + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts here (forwarded to fp64 Jastrow/det zones). + # RT is also forwarded as-is (Principle 3a); cast at the use site below. + dtype_jnp = get_dtype_jnp("coulomb") if Nv == 4: weights = tetrahedron_sym_mesh_Nv4.weights @@ -1726,7 +1734,8 @@ def compute_ecp_non_local_parts_all_pairs( else: raise NotImplementedError - grid_points = grid_points @ RT # rotate the grid points. dim. (N,3) @ (3,3) = (N,3) + # Cast RT to coulomb zone at the use site (Principle 3b). + grid_points = grid_points @ RT.astype(dtype_jnp) # rotate the grid points. dim. (N,3) @ (3,3) = (N,3) # start = time.perf_counter() r_up_carts_on_mesh, r_dn_carts_on_mesh, V_ecp_up, V_ecp_dn, sum_V_nonlocal = ( @@ -1842,11 +1851,12 @@ def compute_ecp_non_local_part_all_pairs_jax_weights_grid_points( """ # V_l_cutoff = 1e-5 - dtype = get_dtype("coulomb") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - weights = jnp.array(weights, dtype=dtype) - grid_points = jnp.array(grid_points, dtype=dtype) + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts here. They are forwarded to + # compute_Jastrow_part / compute_det_geminal_all_elements (fp64 zones); pre-casting + # to the coulomb (fp32) zone would silently down-cast inputs to those fp64 zones. + dtype_jnp = get_dtype_jnp("coulomb") + weights = jnp.array(weights, dtype=dtype_jnp) + grid_points = jnp.array(grid_points, dtype=dtype_jnp) jastrow_denominator = lax.switch( flag_determinant_only, @@ -1863,7 +1873,7 @@ def compute_V_l(r_cart, i_atom, exponent, coefficient, power): structure_data=coulomb_potential_data.structure_data, r_cart=r_cart, i_atom=i_atom, - dtype=dtype, + dtype=dtype_jnp, ) V_l = ( jnp.linalg.norm(rel_R_cart_min_dist) ** -2.0 @@ -1882,7 +1892,7 @@ def compute_P_l_up(ang_mom, r_up_i, r_up_cart, i_atom, weight, vec_delta): structure_data=coulomb_potential_data.structure_data, r_cart=r_up_cart, i_atom=i_atom, - dtype=dtype, + dtype=dtype_jnp, ) r_up_carts_on_mesh = r_up_carts r_up_carts_on_mesh = r_up_carts_on_mesh.at[r_up_i].set( @@ -1902,10 +1912,15 @@ def compute_P_l_up(ang_mom, r_up_i, r_up_cart, i_atom, weight, vec_delta): det_numerator_up = compute_det_geminal_all_elements(wavefunction_data.geminal_data, r_up_carts_on_mesh, r_dn_carts) - wf_ratio_up = jnp.exp(jastrow_numerator_up - jastrow_denominator) * det_numerator_up / det_denominator - # Cast back to coulomb zone: det/jastrow live in fp64 zones and would otherwise - # promote the entire P_l / V_ecp output to fp64. - wf_ratio_up = jnp.asarray(wf_ratio_up, dtype=dtype) + # Consumer-zone explicit cast: cast ALL upstream values to the local + # coulomb zone dtype before any arithmetic, so the wf_ratio computation + # never relies on JAX implicit fp32 x fp64 -> fp64 promotion and never + # borrows another zone's dtype. + jastrow_numerator_up = jnp.asarray(jastrow_numerator_up, dtype=dtype_jnp) + jastrow_denominator_c = jnp.asarray(jastrow_denominator, dtype=dtype_jnp) + det_numerator_up = jnp.asarray(det_numerator_up, dtype=dtype_jnp) + det_denominator_c = jnp.asarray(det_denominator, dtype=dtype_jnp) + wf_ratio_up = jnp.exp(jastrow_numerator_up - jastrow_denominator_c) * det_numerator_up / det_denominator_c P_l_up = (2 * ang_mom + 1) * jnp_legendre_tablated(ang_mom, cos_theta_up) * weight * wf_ratio_up @@ -1919,7 +1934,7 @@ def compute_P_l_dn(ang_mom, r_dn_i, r_dn_cart, i_atom, weight, vec_delta): structure_data=coulomb_potential_data.structure_data, r_cart=r_dn_cart, i_atom=i_atom, - dtype=dtype, + dtype=dtype_jnp, ) r_dn_carts_on_mesh = r_dn_carts r_dn_carts_on_mesh = r_dn_carts_on_mesh.at[r_dn_i].set( @@ -1939,9 +1954,12 @@ def compute_P_l_dn(ang_mom, r_dn_i, r_dn_cart, i_atom, weight, vec_delta): det_numerator_dn = compute_det_geminal_all_elements(wavefunction_data.geminal_data, r_up_carts, r_dn_carts_on_mesh) - wf_ratio_dn = jnp.exp(jastrow_numerator_dn - jastrow_denominator) * det_numerator_dn / det_denominator - # Cast back to coulomb zone (see compute_P_l_up). - wf_ratio_dn = jnp.asarray(wf_ratio_dn, dtype=dtype) + # Consumer-zone explicit cast (see compute_P_l_up). + jastrow_numerator_dn = jnp.asarray(jastrow_numerator_dn, dtype=dtype_jnp) + jastrow_denominator_c = jnp.asarray(jastrow_denominator, dtype=dtype_jnp) + det_numerator_dn = jnp.asarray(det_numerator_dn, dtype=dtype_jnp) + det_denominator_c = jnp.asarray(det_denominator, dtype=dtype_jnp) + wf_ratio_dn = jnp.exp(jastrow_numerator_dn - jastrow_denominator_c) * det_numerator_dn / det_denominator_c P_l_dn = (2 * ang_mom + 1) * jnp_legendre_tablated(ang_mom, cos_theta_dn) * weight * wf_ratio_dn return r_dn_carts_on_mesh, P_l_dn @@ -2010,16 +2028,19 @@ def compute_V_nonlocal_dn( ) # Vectrized (flatten) arguments are prepared here. + # NOTE: Keep r_up_carts/r_dn_carts as fp64 here; the inner closures (compute_P_l_*) + # forward derived mesh coords to fp64 Jastrow/det zones, so any fp32 down-cast here + # would propagate to those zones. r_up_i_jnp = jnp.arange(len(r_up_carts)) - r_up_carts_jnp = jnp.asarray(r_up_carts, dtype=dtype) + r_up_carts_jnp = r_up_carts r_dn_i_jnp = jnp.arange(len(r_dn_carts)) - r_dn_carts_jnp = jnp.asarray(r_dn_carts, dtype=dtype) + r_dn_carts_jnp = r_dn_carts i_atom_np = jnp.array(coulomb_potential_data._nucleus_index_non_local_part) ang_mom_np = jnp.array(coulomb_potential_data._ang_mom_non_local_part) - exponent_np = jnp.array(coulomb_potential_data._exponents_non_local_part, dtype=dtype) - coefficient_np = jnp.array(coulomb_potential_data._coefficients_non_local_part, dtype=dtype) - power_np = jnp.array(coulomb_potential_data._powers_non_local_part, dtype=dtype) + exponent_np = jnp.array(coulomb_potential_data._exponents_non_local_part, dtype=dtype_jnp) + coefficient_np = jnp.array(coulomb_potential_data._coefficients_non_local_part, dtype=dtype_jnp) + power_np = jnp.array(coulomb_potential_data._powers_non_local_part, dtype=dtype_jnp) r_up_carts_on_mesh, V_ecp_up = vmap_vmap_compute_ecp_up( r_up_i_jnp, @@ -2070,10 +2091,8 @@ def compute_ecp_coulomb_potential( Returns: float: Sum of local and non-local ECP contributions for the given geometry. """ - dtype = get_dtype("coulomb") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - RT = jnp.asarray(RT, dtype=dtype) + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts/RT here (forwarded to downstream + # functions that handle their own use-site casts — Principle 3a). ecp_local_parts = compute_ecp_local_parts_all_pairs( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts @@ -2146,10 +2165,8 @@ def compute_ecp_coulomb_potential_fast( :func:`compute_ecp_non_local_parts_nearest_neighbors_fast_update` becomes incorrect and the non-local ratios will be silently wrong. """ - dtype = get_dtype("coulomb") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - RT = jnp.asarray(RT, dtype=dtype) + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts/RT here (forwarded to downstream + # functions that handle their own use-site casts — Principle 3a). ecp_local_parts = compute_ecp_local_parts_all_pairs( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts @@ -2178,10 +2195,9 @@ def _compute_bare_coulomb_potential_debug( r_dn_carts: npt.NDArray[np.float64], ) -> float: """See compute_bare_coulomb_potential_api.""" - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) - r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) + # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). The + # accumulated scalar is cast to the coulomb zone before return (Principle 3b). + dtype_np = get_dtype_np("coulomb") R_carts = coulomb_potential_data.structure_data._positions_cart_np R_charges = coulomb_potential_data._effective_charges @@ -2198,7 +2214,7 @@ def _compute_bare_coulomb_potential_debug( ] ) - return bare_coulomb_potential + return np.asarray(bare_coulomb_potential, dtype=dtype_np) @jit @@ -2217,10 +2233,8 @@ def compute_bare_coulomb_potential( Returns: float: Total bare Coulomb energy. """ - dtype = get_dtype("coulomb") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts here. Downstream el_ion / el_el helpers + # reconstruct r_i - r_j in fp64 internally; pre-casting would destroy that precision. interactions_ion_ion = compute_bare_coulomb_potential_ion_ion(coulomb_potential_data) interactions_el_ion_elements_up, interactions_el_ion_elements_dn = compute_bare_coulomb_potential_el_ion_element_wise( coulomb_potential_data, r_up_carts, r_dn_carts @@ -2251,20 +2265,22 @@ def compute_bare_coulomb_potential_el_ion_element_wise( Returns: tuple[jax.Array, jax.Array]: Element-wise ion–electron interactions for up spins and down spins (shape ``(N_up,)`` and ``(N_dn,)``). """ - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype) + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts. el_ion_interaction reconstructs + # r_i - r_j in fp64 internally to avoid catastrophic cancellation; a fp32 pre-cast + # here would silently destroy that precision before the reconstruction can take effect. + dtype_jnp = get_dtype_jnp("coulomb") + dtype_np = get_dtype_np("coulomb") + R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype_jnp) R_charges = np.array(coulomb_potential_data._effective_charges, dtype=dtype_np) r_up_charges = np.full(len(r_up_carts), -1.0, dtype=dtype_np) r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=dtype_np) - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - # Define a function to compute interaction for a pair def el_ion_interaction(Z_i, Z_j, r_i, r_j): - # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. - diff = (r_i.astype(jnp.float64) - r_j.astype(jnp.float64)).astype(r_i.dtype) + # Reconstruct r_i - r_j in caller-supplied precision (fp64 from MCMC walker + # state) via JAX promotion when one operand is fp64, then downcast to the + # coulomb zone. Avoids catastrophic cancellation without hardcoding fp64. + diff = (r_i - r_j).astype(dtype_jnp) distance = jnp.linalg.norm(diff, axis=1) interaction = (Z_i * Z_j) / distance return interaction @@ -2298,20 +2314,21 @@ def compute_discretized_bare_coulomb_potential_el_ion_element_wise( Returns: tuple[jax.Array, jax.Array]: Element-wise ion–electron interactions for up spins and down spins (shape ``(N_up,)`` and ``(N_dn,)``). """ - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype) + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts. el_ion_interaction reconstructs + # r_i - r_j in fp64 internally to avoid catastrophic cancellation. + dtype_jnp = get_dtype_jnp("coulomb") + dtype_np = get_dtype_np("coulomb") + R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype_jnp) R_charges = np.array(coulomb_potential_data._effective_charges, dtype=dtype_np) r_up_charges = np.full(len(r_up_carts), -1.0, dtype=dtype_np) r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=dtype_np) - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - # Define a function to compute interaction for a pair def el_ion_interaction(Z_i, Z_j, r_i, r_j, alat): - # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. - diff = (r_i.astype(jnp.float64) - r_j.astype(jnp.float64)).astype(r_i.dtype) + # Reconstruct r_i - r_j in caller-supplied precision (fp64 from MCMC walker + # state) via JAX promotion when one operand is fp64, then downcast to the + # coulomb zone. Avoids catastrophic cancellation without hardcoding fp64. + diff = (r_i - r_j).astype(dtype_jnp) distance = jnp.maximum(jnp.linalg.norm(diff, axis=1), alat) interaction = (Z_i * Z_j) / distance return interaction @@ -2335,12 +2352,10 @@ def _compute_bare_coulomb_potential_el_ion_element_wise_debug( r_dn_carts: npt.NDArray[np.float64], ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """See compute_bare_coulomb_potential_api.""" - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) - r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) - - R_carts = coulomb_potential_data.structure_data._positions_cart_np + # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). The + # accumulators are cast to the coulomb zone before return (Principle 3b). + dtype_np = get_dtype_np("coulomb") + R_carts = coulomb_potential_data.structure_data._positions_cart_np # fp64 storage accessor R_charges = coulomb_potential_data._effective_charges r_up_charges = [-1 for _ in range(len(r_up_carts))] r_dn_charges = [-1 for _ in range(len(r_dn_carts))] @@ -2349,9 +2364,11 @@ def _compute_bare_coulomb_potential_el_ion_element_wise_debug( interactions_R_r_dn = np.zeros(len(r_dn_carts)) for i, (r_up_charge, r_up_cart) in enumerate(zip(r_up_charges, r_up_carts, strict=True)): + # Reconstruct R - r in caller-supplied precision then downcast to the + # coulomb zone at the use site (Principle 3b). interactions_R_r_up[i] = np.sum( [ - (R_charge * r_up_charge) / np.linalg.norm(R_cart - r_up_cart) + (R_charge * r_up_charge) / np.linalg.norm((R_cart - r_up_cart).astype(dtype_np)) for R_charge, R_cart in zip(R_charges, R_carts, strict=True) ] ) @@ -2359,12 +2376,15 @@ def _compute_bare_coulomb_potential_el_ion_element_wise_debug( for i, (r_dn_charge, r_dn_cart) in enumerate(zip(r_dn_charges, r_dn_carts, strict=True)): interactions_R_r_dn[i] = np.sum( [ - (R_charge * r_dn_charge) / np.linalg.norm(R_cart - r_dn_cart) + (R_charge * r_dn_charge) / np.linalg.norm((R_cart - r_dn_cart).astype(dtype_np)) for R_charge, R_cart in zip(R_charges, R_carts, strict=True) ] ) - return interactions_R_r_up, interactions_R_r_dn + return ( + np.asarray(interactions_R_r_up, dtype=dtype_np), + np.asarray(interactions_R_r_dn, dtype=dtype_np), + ) def _compute_discretized_bare_coulomb_potential_el_ion_element_wise_debug( @@ -2374,12 +2394,10 @@ def _compute_discretized_bare_coulomb_potential_el_ion_element_wise_debug( alat: float, ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """See compute_bare_coulomb_potential_api.""" - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) - r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) - - R_carts = coulomb_potential_data.structure_data._positions_cart_np + # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). The + # accumulators are cast to the coulomb zone before return (Principle 3b). + dtype_np = get_dtype_np("coulomb") + R_carts = coulomb_potential_data.structure_data._positions_cart_np # fp64 storage accessor R_charges = coulomb_potential_data._effective_charges r_up_charges = [-1 for _ in range(len(r_up_carts))] r_dn_charges = [-1 for _ in range(len(r_dn_carts))] @@ -2388,9 +2406,11 @@ def _compute_discretized_bare_coulomb_potential_el_ion_element_wise_debug( interactions_R_r_dn = np.zeros(len(r_dn_carts)) for i, (r_up_charge, r_up_cart) in enumerate(zip(r_up_charges, r_up_carts, strict=True)): + # Reconstruct R - r in caller-supplied precision then downcast to the + # coulomb zone at the use site (Principle 3b). interactions_R_r_up[i] = np.sum( [ - (R_charge * r_up_charge) / np.maximum(np.linalg.norm(R_cart - r_up_cart), alat) + (R_charge * r_up_charge) / np.maximum(np.linalg.norm((R_cart - r_up_cart).astype(dtype_np)), alat) for R_charge, R_cart in zip(R_charges, R_carts, strict=True) ] ) @@ -2398,12 +2418,15 @@ def _compute_discretized_bare_coulomb_potential_el_ion_element_wise_debug( for i, (r_dn_charge, r_dn_cart) in enumerate(zip(r_dn_charges, r_dn_carts, strict=True)): interactions_R_r_dn[i] = np.sum( [ - (R_charge * r_dn_charge) / np.maximum(np.linalg.norm(R_cart - r_dn_cart), alat) + (R_charge * r_dn_charge) / np.maximum(np.linalg.norm((R_cart - r_dn_cart).astype(dtype_np)), alat) for R_charge, R_cart in zip(R_charges, R_carts, strict=True) ] ) - return interactions_R_r_up, interactions_R_r_dn + return ( + np.asarray(interactions_R_r_up, dtype=dtype_np), + np.asarray(interactions_R_r_dn, dtype=dtype_np), + ) @jit @@ -2420,14 +2443,13 @@ def compute_bare_coulomb_potential_el_el( Returns: float: Electron–electron Coulomb energy. """ - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts. el_el_interaction reconstructs + # r_i - r_j in fp64 internally to avoid catastrophic cancellation. + dtype_np = get_dtype_np("coulomb") + dtype_jnp = get_dtype_jnp("coulomb") r_up_charges = np.full(len(r_up_carts), -1.0, dtype=dtype_np) r_dn_charges = np.full(len(r_dn_carts), -1.0, dtype=dtype_np) - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - all_charges = np.hstack([r_up_charges, r_dn_charges]) all_carts = jnp.vstack([r_up_carts, r_dn_carts]) @@ -2447,8 +2469,10 @@ def compute_bare_coulomb_potential_el_el( # Define a function to compute interaction for a pair def el_el_interaction(Z_i, Z_j, r_i, r_j): - # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. - diff = (r_i.astype(jnp.float64) - r_j.astype(jnp.float64)).astype(r_i.dtype) + # Reconstruct r_i - r_j in caller-supplied precision (fp64 from MCMC walker + # state) via JAX promotion when one operand is fp64, then downcast to the + # coulomb zone. Avoids catastrophic cancellation without hardcoding fp64. + diff = (r_i - r_j).astype(dtype_jnp) distance = jnp.linalg.norm(diff) interaction = (Z_i * Z_j) / distance return interaction @@ -2474,9 +2498,9 @@ def compute_bare_coulomb_potential_ion_ion( Returns: float: Ion–ion Coulomb energy. """ - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype) + dtype_jnp = get_dtype_jnp("coulomb") + dtype_np = get_dtype_np("coulomb") + R_carts = jnp.array(coulomb_potential_data.structure_data._positions_cart_jnp, dtype=dtype_jnp) R_charges = np.array(coulomb_potential_data._effective_charges, dtype=dtype_np) all_charges = R_charges @@ -2498,8 +2522,10 @@ def compute_bare_coulomb_potential_ion_ion( # Define a function to compute interaction for a pair def ion_ion_interaction(Z_i, Z_j, r_i, r_j): - # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. - diff = (r_i.astype(jnp.float64) - r_j.astype(jnp.float64)).astype(r_i.dtype) + # Reconstruct r_i - r_j in caller-supplied precision (fp64 from MCMC walker + # state) via JAX promotion when one operand is fp64, then downcast to the + # coulomb zone. Avoids catastrophic cancellation without hardcoding fp64. + diff = (r_i - r_j).astype(dtype_jnp) distance = jnp.linalg.norm(diff) interaction = (Z_i * Z_j) / distance return interaction @@ -2529,10 +2555,7 @@ def compute_bare_coulomb_potential_el_ion( Returns: float: Electron–ion Coulomb energy. """ - dtype = get_dtype("coulomb") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts (forwarded to el_ion_element_wise which reconstructs in fp64). interactions_el_ion_elements_up, interactions_el_ion_elements_dn = compute_bare_coulomb_potential_el_ion_element_wise( coulomb_potential_data, r_up_carts, r_dn_carts ) @@ -2550,11 +2573,10 @@ def _compute_coulomb_potential_debug( wavefunction_data: Wavefunction_data = None, ) -> float: """See compute_coulomb_potential_api.""" - dtype = get_dtype("coulomb") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) - r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) - RT = np.asarray(RT, dtype=dtype_np) + # Forward r_up/dn_carts and RT as-is (Principle 3a — no parameter rebind). + # Each downstream debug function casts to its own zone at the use site; + # the accumulated scalar is cast to the coulomb zone before return. + dtype_np = get_dtype_np("coulomb") # all-electron if not coulomb_potential_data.ecp_flag: @@ -2583,7 +2605,7 @@ def _compute_coulomb_potential_debug( NN=NN, ) - return bare_coulomb_potential + ecp_coulomb_potential + return np.asarray(bare_coulomb_potential + ecp_coulomb_potential, dtype=dtype_np) def compute_coulomb_potential( @@ -2609,10 +2631,8 @@ def compute_coulomb_potential( Returns: float: Sum of bare Coulomb (ion–ion, electron–ion, electron–electron) and ECP (local + non-local) energies. """ - dtype = get_dtype("coulomb") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - RT = jnp.asarray(RT, dtype=dtype) + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts/RT (forwarded to downstream + # functions that handle their own use-site casts — Principle 3a). # all-electron if not coulomb_potential_data.ecp_flag: @@ -2679,10 +2699,8 @@ def compute_coulomb_potential_fast( electrons have moved simultaneously the underlying Sherman–Morrison rank-1 update is incorrect and non-local ratios will be silently wrong. """ - dtype = get_dtype("coulomb") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - RT = jnp.asarray(RT, dtype=dtype) + # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts/RT (forwarded to downstream + # functions that handle their own use-site casts — Principle 3a). # all-electron — no ECP, no need for A_old_inv if not coulomb_potential_data.ecp_flag: diff --git a/jqmc/determinant.py b/jqmc/determinant.py index 22ea852d..c5468473 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -51,14 +51,13 @@ # from jax.debug import print as jprint import jax import jax.numpy as jnp -import jax.scipy.linalg as jsp_linalg import numpy as np import numpy.typing as npt from flax import struct from jax import jit, vmap -from ._precision import get_dtype -from ._setting import EPS_rcond_SVD, atol_consistency, get_eps, rtol_consistency +from ._precision import get_dtype_jnp, get_dtype_np +from ._setting import atol_consistency, get_eps, rtol_consistency from .atomic_orbital import ( AOs_cart_data, AOs_sphe_data, @@ -93,8 +92,8 @@ class Geminal_data: num_electron_dn (int): Number of spin-down electrons. orb_data_up_spin (AOs_data | MOs_data): Basis/orbitals for spin-up electrons. orb_data_dn_spin (AOs_data | MOs_data): Basis/orbitals for spin-down electrons. - lambda_matrix (npt.NDArray | jax.Array): Geminal pairing matrix with shape - ``(orb_num_up, orb_num_dn + num_electron_up - num_electron_dn)``. + lambda_matrix (npt.NDArray[np.float64]): Geminal pairing matrix with shape + ``(orb_num_up, orb_num_dn + num_electron_up - num_electron_dn)``. dtype: float64. Notes: - For closed shells, ``orb_num_up == orb_num_dn`` and ``lambda_matrix`` is square. @@ -109,9 +108,9 @@ class Geminal_data: orb_data_dn_spin: AOs_sphe_data | AOs_cart_data | MOs_data = struct.field( pytree_node=True, default_factory=lambda: AOs_sphe_data() ) #: Orbital data (AOs or MOs) for spin-down electrons. - lambda_matrix: npt.NDArray | jax.Array = struct.field( - pytree_node=True, default_factory=lambda: np.array([]) - ) #: Geminal pairing matrix; see class notes for expected shape. + lambda_matrix: npt.NDArray[np.float64] = struct.field( + pytree_node=True, default_factory=lambda: np.array([], dtype=np.float64) + ) #: Geminal pairing matrix; see class notes for expected shape. dtype: float64. def sanity_check(self) -> None: """Check attributes of the class. @@ -166,43 +165,50 @@ def _logger_info(self) -> None: # --- AO basis property accessors (for basis optimization) --- + @property + def _lambda_matrix_jnp(self) -> jax.Array: + """Return lambda_matrix as a jax.Array (jnp view of the underlying numpy storage).""" + # Lift-only fp64 basis-data storage accessor (see _precision.py exemption); + # consumer casts to its own zone at use site. + return jnp.asarray(self.lambda_matrix, dtype=jnp.float64) + @property def ao_exponents_up(self) -> jax.Array: - """AO Gaussian exponents for spin-up orbitals, regardless of AO/MO representation.""" + """AO Gaussian exponents for spin-up orbitals (jnp view of underlying numpy storage).""" if isinstance(self.orb_data_up_spin, (AOs_sphe_data, AOs_cart_data)): - return self.orb_data_up_spin.exponents + return self.orb_data_up_spin._exponents_jnp elif isinstance(self.orb_data_up_spin, MOs_data): - return self.orb_data_up_spin.aos_data.exponents + return self.orb_data_up_spin.aos_data._exponents_jnp else: raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_up_spin)}") @property def ao_exponents_dn(self) -> jax.Array: - """AO Gaussian exponents for spin-down orbitals, regardless of AO/MO representation.""" + """AO Gaussian exponents for spin-down orbitals (jnp view of underlying numpy storage).""" if isinstance(self.orb_data_dn_spin, (AOs_sphe_data, AOs_cart_data)): - return self.orb_data_dn_spin.exponents + return self.orb_data_dn_spin._exponents_jnp elif isinstance(self.orb_data_dn_spin, MOs_data): - return self.orb_data_dn_spin.aos_data.exponents + return self.orb_data_dn_spin.aos_data._exponents_jnp else: raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_dn_spin)}") @property def ao_coefficients_up(self) -> jax.Array: - """AO contraction coefficients for spin-up orbitals, regardless of AO/MO representation.""" + """AO contraction coefficients for spin-up orbitals (jnp view of underlying numpy storage).""" if isinstance(self.orb_data_up_spin, (AOs_sphe_data, AOs_cart_data)): - return self.orb_data_up_spin.coefficients + return self.orb_data_up_spin._coefficients_jnp elif isinstance(self.orb_data_up_spin, MOs_data): - return self.orb_data_up_spin.aos_data.coefficients + return self.orb_data_up_spin.aos_data._coefficients_jnp else: raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_up_spin)}") @property def ao_coefficients_dn(self) -> jax.Array: - """AO contraction coefficients for spin-down orbitals, regardless of AO/MO representation.""" + """AO contraction coefficients for spin-down orbitals (jnp view of underlying numpy storage).""" if isinstance(self.orb_data_dn_spin, (AOs_sphe_data, AOs_cart_data)): - return self.orb_data_dn_spin.coefficients + return self.orb_data_dn_spin._coefficients_jnp elif isinstance(self.orb_data_dn_spin, MOs_data): - return self.orb_data_dn_spin.aos_data.coefficients + return self.orb_data_dn_spin.aos_data._coefficients_jnp else: raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_dn_spin)}") @@ -226,14 +232,18 @@ def _replace_orb_coefficients(self, orb_data, new_coeff): else: raise NotImplementedError(f"Unsupported orb_data type: {type(orb_data)}") - def with_updated_ao_exponents(self, new_exp_up: jax.Array, new_exp_dn: jax.Array) -> "Geminal_data": + def with_updated_ao_exponents( + self, new_exp_up: npt.NDArray[np.float64], new_exp_dn: npt.NDArray[np.float64] + ) -> "Geminal_data": """Return a new instance with updated AO exponents for both spins.""" return self.replace( orb_data_up_spin=self._replace_orb_exponents(self.orb_data_up_spin, new_exp_up), orb_data_dn_spin=self._replace_orb_exponents(self.orb_data_dn_spin, new_exp_dn), ) - def with_updated_ao_coefficients(self, new_coeff_up: jax.Array, new_coeff_dn: jax.Array) -> "Geminal_data": + def with_updated_ao_coefficients( + self, new_coeff_up: npt.NDArray[np.float64], new_coeff_dn: npt.NDArray[np.float64] + ) -> "Geminal_data": """Return a new instance with updated AO contraction coefficients for both spins.""" return self.replace( orb_data_up_spin=self._replace_orb_coefficients(self.orb_data_up_spin, new_coeff_up), @@ -286,14 +296,12 @@ def apply_block_update(self, block: "VariationalParameterBlock") -> "Geminal_dat elif block.name == "lambda_basis_exp": vals = np.asarray(block.values, dtype=np.float64) vals = self._symmetrize_ao_basis(vals) - vals = jnp.asarray(vals, dtype=jnp.float64) n_up = len(self.ao_exponents_up) new_exp_up, new_exp_dn = vals[:n_up], vals[n_up:] return self.with_updated_ao_exponents(new_exp_up, new_exp_dn) elif block.name == "lambda_basis_coeff": vals = np.asarray(block.values, dtype=np.float64) vals = self._symmetrize_ao_basis(vals) - vals = jnp.asarray(vals, dtype=jnp.float64) n_up = len(self.ao_coefficients_up) new_coeff_up, new_coeff_dn = vals[:n_up], vals[n_up:] return self.with_updated_ao_coefficients(new_coeff_up, new_coeff_dn) @@ -1007,9 +1015,9 @@ def compute_ln_det_geminal_all_elements( Returns: float: Scalar log-determinant of the geminal matrix. """ - dtype = get_dtype("determinant") + dtype_jnp = get_dtype_jnp("det_eval") G = compute_geminal_all_elements(geminal_data=geminal_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - G = jnp.asarray(G, dtype=dtype) + G = jnp.asarray(G, dtype=dtype_jnp) return jnp.log(jnp.abs(jnp.linalg.det(G))) @@ -1026,9 +1034,9 @@ def _ln_det_fwd(geminal_data, r_up_carts, r_dn_carts): - primal output: ln|det(G)| - residuals: (inputs and SVD factors) for use in backward pass """ - dtype = get_dtype("determinant") + dtype_jnp = get_dtype_jnp("det_eval") G = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) - G = jnp.asarray(G, dtype=dtype) + G = jnp.asarray(G, dtype=dtype_jnp) ln_det = jnp.log(jnp.abs(jnp.linalg.det(G))) # Compute SVD: G = U_svd @ diag(s) @ Vt U_svd, s, Vt = jnp.linalg.svd(G, full_matrices=False) @@ -1060,9 +1068,9 @@ def _ln_det_bwd(res, g): # Compute G^{-1} via SVD pseudoinverse with thresholding. # Singular values below eps_rcond * s_max are zeroed to avoid NaN from 1/~0. - dtype = get_dtype("determinant") - eps_rcond = get_eps("rcond_svd", dtype) - s_inv = jnp.where(s > eps_rcond * s[0], jnp.asarray(1.0, dtype=dtype) / s, jnp.asarray(0.0, dtype=dtype)) + dtype_jnp = get_dtype_jnp("det_eval") + eps_rcond = get_eps("rcond_svd", dtype_jnp) + s_inv = jnp.where(s > eps_rcond * s[0], jnp.asarray(1.0, dtype=dtype_jnp) / s, jnp.asarray(0.0, dtype=dtype_jnp)) X = (Vt.T * s_inv[jnp.newaxis, :]) @ U_svd.T # G^{-1}, shape (n, n) # d ln|det G| / dG = (G^{-1})^T, scaled by incoming cotangent g @@ -1111,9 +1119,9 @@ def compute_ln_det_geminal_all_elements_fast( used in the MCMC loop. Passing an inverse that corresponds to different electron positions silently produces incorrect gradients. """ - dtype = get_dtype("determinant") + dtype_jnp = get_dtype_jnp("det_eval") G = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) - G = jnp.asarray(G, dtype=dtype) + G = jnp.asarray(G, dtype=dtype_jnp) return jnp.log(jnp.abs(jnp.linalg.det(G))) @@ -1123,18 +1131,18 @@ def _ln_det_fast_fwd( r_dn_carts: jax.Array, geminal_inv: jax.Array, ): - dtype = get_dtype("determinant") + dtype_jnp = get_dtype_jnp("det_eval") G = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) - G = jnp.asarray(G, dtype=dtype) + G = jnp.asarray(G, dtype=dtype_jnp) val = jnp.log(jnp.abs(jnp.linalg.det(G))) # Save inputs for backward (geminal_inv replaces G^{-1} in bwd) return val, (geminal_data, r_up_carts, r_dn_carts, geminal_inv) def _ln_det_fast_bwd(res, g): - dtype = get_dtype("determinant") + dtype_jnp = get_dtype_jnp("det_eval") geminal_data, r_up_carts, r_dn_carts, geminal_inv = res - geminal_inv = jnp.asarray(geminal_inv, dtype=dtype) + geminal_inv = jnp.asarray(geminal_inv, dtype=dtype_jnp) # d(ln|det G|)/d(G_{ij}) = (G^{-T})_{ij} # Use the pre-computed inverse instead of re-solving. G_bar = g * geminal_inv.T # cotangent w.r.t. G, shape (N_up, N_up) @@ -1165,9 +1173,9 @@ def compute_det_geminal_all_elements( Returns: float: Scalar determinant of the geminal matrix. """ - dtype = get_dtype("determinant") + dtype_jnp = get_dtype_jnp("det_eval") G = compute_geminal_all_elements(geminal_data=geminal_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - G = jnp.asarray(G, dtype=dtype) + G = jnp.asarray(G, dtype=dtype_jnp) return jnp.linalg.det(G) @@ -1177,8 +1185,7 @@ def _compute_det_geminal_all_elements_debug( r_dn_carts: npt.NDArray[np.float64], ) -> np.float64: """See compute_det_geminal_all_elements_api.""" - dtype = get_dtype("determinant") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("det_eval") G = _compute_geminal_all_elements_debug( geminal_data=geminal_data, r_up_carts=r_up_carts, @@ -1200,9 +1207,9 @@ def compute_AS_regularization_factor_fast_update( Returns: jax.Array: Scalar AS regularization factor. """ - dtype = get_dtype("determinant") - geminal = jnp.asarray(geminal, dtype=dtype) - geminal_inv = jnp.asarray(geminal_inv, dtype=dtype) + dtype_jnp = get_dtype_jnp("det_ratio") + geminal = jnp.asarray(geminal, dtype=dtype_jnp) + geminal_inv = jnp.asarray(geminal_inv, dtype=dtype_jnp) # compute the AS factor theta = 3.0 / 8.0 @@ -1232,8 +1239,7 @@ def _compute_AS_regularization_factor_debug( geminal_data: Geminal_data, r_up_carts: npt.NDArray[np.float64], r_dn_carts: npt.NDArray[np.float64] ) -> npt.NDArray[np.float64]: """See compute_AS_regularization_factor_jax.""" - dtype = get_dtype("determinant") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("det_eval") geminal = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) geminal = np.asarray(geminal, dtype=dtype_np) @@ -1268,19 +1274,19 @@ def compute_AS_regularization_factor(geminal_data: Geminal_data, r_up_carts: jax Returns: jax.Array: Scalar AS regularization factor. """ - dtype = get_dtype("determinant") + dtype_jnp = get_dtype_jnp("det_eval") geminal = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) - geminal = jnp.asarray(geminal, dtype=dtype) + geminal = jnp.asarray(geminal, dtype=dtype_jnp) # compute the AS factor theta = 3.0 / 8.0 # compute F \equiv the square of Frobenius norm of geminal_inv # Use SVD with conservative threshold to avoid Inf from 1/sigma^2 for tiny sigma - eps_rcond = get_eps("rcond_svd", dtype) + eps_rcond = get_eps("rcond_svd", dtype_jnp) sigma = jnp.linalg.svd(geminal, compute_uv=False) sigma_sq_inv = jnp.where( - sigma > eps_rcond * sigma[0], jnp.asarray(1.0, dtype=dtype) / (sigma**2), jnp.asarray(0.0, dtype=dtype) + sigma > eps_rcond * sigma[0], jnp.asarray(1.0, dtype=dtype_jnp) / (sigma**2), jnp.asarray(0.0, dtype=dtype_jnp) ) F = jnp.sum(sigma_sq_inv) @@ -1313,9 +1319,13 @@ def compute_geminal_all_elements(geminal_data: Geminal_data, r_up_carts: jax.Arr Returns: jax.Array: Geminal matrix with shape ``(N_up, N_up)`` combining paired and unpaired blocks. """ - dtype = get_dtype("geminal") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + # NOTE: do not pre-cast r_*_carts here. r_*_carts is only forwarded to + # ``_compute_geminal_all_elements`` (which in turn calls ``compute_orb_api`` + # → ``compute_AOs``); the AO kernels reconstruct ``r - R`` in float64 + # internally to avoid catastrophic cancellation, and a wrapper-level + # downcast would defeat that guard. Arithmetic in this function uses + # ``ao_matrix_*`` / ``lambda_matrix_*`` which are cast at their own use + # sites, so r_*_carts itself does not need casting here. if len(r_up_carts) != geminal_data.num_electron_up or len(r_dn_carts) != geminal_data.num_electron_dn: logger.info( f"Number of up and dn electrons (N_up, N_dn) = ({len(r_up_carts)}, {len(r_dn_carts)}) are not consistent " @@ -1348,13 +1358,16 @@ def _compute_geminal_all_elements( r_dn_carts: jax.Array, ) -> jax.Array: """See compute_geminal_all_elements_api.""" - dtype = get_dtype("geminal") - lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data.lambda_matrix, [geminal_data.orb_num_dn]) - lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) - lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) + dtype_jnp = get_dtype_jnp("det_eval") + lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) - orb_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) - orb_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) + # orb_matrix_* may be produced in the orb_eval zone (potentially float32). + # Explicitly upcast to the geminal zone here so the matmul does not rely + # on JAX implicit type promotion (fp32 x fp64 -> fp64). + orb_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) + orb_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) # compute geminal values geminal_paired = jnp.dot(orb_matrix_up.T, jnp.dot(lambda_matrix_paired, orb_matrix_dn)) @@ -1370,16 +1383,15 @@ def _compute_geminal_all_elements_debug( r_dn_carts: npt.NDArray[np.float64], ) -> npt.NDArray[np.float64]: """See compute_geminal_all_elements_api.""" - dtype = get_dtype("geminal") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("det_eval") r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) lambda_matrix_paired, lambda_matrix_unpaired = np.hsplit(geminal_data.lambda_matrix, [geminal_data.orb_num_dn]) lambda_matrix_paired = np.asarray(lambda_matrix_paired, dtype=dtype_np) lambda_matrix_unpaired = np.asarray(lambda_matrix_unpaired, dtype=dtype_np) - orb_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) - orb_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) + orb_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_np) + orb_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_np) # compute geminal values geminal_paired = np.dot(orb_matrix_up.T, np.dot(lambda_matrix_paired, orb_matrix_dn)) @@ -1407,24 +1419,27 @@ def compute_geminal_up_one_row_elements( Returns: jax.Array: Row vector with shape ``(N_dn + N_unpaired,)``. """ - dtype = get_dtype("geminal") - r_up_cart = jnp.asarray(r_up_cart, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + # r_*_cart(s) are only forwarded to ``compute_orb_api``; do not pre-cast + # here (would defeat the AO kernels' fp64 ``r - R`` reconstruction). + dtype_jnp = get_dtype_jnp("det_ratio") # Split lambda into paired/unpaired blocks along columns lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit( - geminal_data.lambda_matrix, [geminal_data.orb_num_dn] + geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn] ) # shapes: (n_orb_up, n_orb_dn), (n_orb_up, num_unpaired) - lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) - lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) # Orbital values: # - up: single position -> 1D vector (n_orb_up,) # - dn: batched positions -> (n_orb_dn, N_dn) - orb_up_vec = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_cart) + # Explicitly upcast to the geminal zone (compute_orb_api may return + # orb_eval dtype, e.g. fp32 for AGP) to avoid relying on JAX implicit + # type promotion in the lambda matmul below. + orb_up_vec = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_cart).astype(dtype_jnp) orb_up_vec = jnp.reshape(orb_up_vec, (-1,)) # ensure (n_orb_up,) - orb_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) + orb_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) # ensure (n_orb_dn, N_dn) - orb_matrix_dn = jnp.asarray(orb_matrix_dn) + orb_matrix_dn = jnp.asarray(orb_matrix_dn, dtype=dtype_jnp) # Paired block row: (n_orb_up,) @ (n_orb_up, N_dn) -> (N_dn,) paired_right = lambda_matrix_paired @ orb_matrix_dn # (n_orb_up, N_dn) @@ -1456,22 +1471,25 @@ def compute_geminal_dn_one_column_elements( Returns: jax.Array: Column vector for the paired block with shape ``(N_up,)``. """ - dtype = get_dtype("geminal") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_cart = jnp.asarray(r_dn_cart, dtype=dtype) + # r_*_cart(s) are only forwarded to ``compute_orb_api``; do not pre-cast + # here (would defeat the AO kernels' fp64 ``r - R`` reconstruction). + dtype_jnp = get_dtype_jnp("det_ratio") # Split lambda into paired/unpaired blocks along columns lambda_matrix_paired, _lambda_matrix_unpaired = jnp.hsplit( - geminal_data.lambda_matrix, [geminal_data.orb_num_dn] + geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn] ) # lambda_matrix_paired: (n_orb_up, n_orb_dn) - lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) # Orbital values: # - up: batched positions -> (n_orb_up, N_up) # - dn: single position -> 1D vector (n_orb_dn,) - orb_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) - orb_matrix_up = jnp.asarray(orb_matrix_up) # (n_orb_up, N_up) + # Explicitly upcast to the geminal zone (compute_orb_api may return + # orb_eval dtype, e.g. fp32 for AGP) to avoid relying on JAX implicit + # type promotion in the lambda matmul below. + orb_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) + orb_matrix_up = jnp.asarray(orb_matrix_up, dtype=dtype_jnp) # (n_orb_up, N_up) - orb_dn_vec = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_cart) + orb_dn_vec = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_cart).astype(dtype_jnp) orb_dn_vec = jnp.reshape(orb_dn_vec, (-1,)) # (n_orb_dn,) # Column of paired block: @@ -1520,12 +1538,11 @@ def _compute_ratio_determinant_part_rank1_update( grid generated by the MCMC loop, where exactly one electron is displaced per grid point by construction. """ - dtype = get_dtype("geminal") - A_old_inv = jnp.asarray(A_old_inv, dtype=dtype) - old_r_up_carts = jnp.asarray(old_r_up_carts, dtype=dtype) - old_r_dn_carts = jnp.asarray(old_r_dn_carts, dtype=dtype) - new_r_up_carts_arr = jnp.asarray(new_r_up_carts_arr, dtype=dtype) - new_r_dn_carts_arr = jnp.asarray(new_r_dn_carts_arr, dtype=dtype) + # Forward A_old_inv and old/new r_up/dn_carts as-is (Principle 3a — no + # parameter rebind). Module-level forwards (compute_det_geminal_all_elements, + # compute_orb_api) handle their own use-site casts. Inline arithmetic below + # casts at the use site (Principle 3b) — see jnp.dot with A_old_inv. + dtype_jnp = get_dtype_jnp("det_ratio") num_up = old_r_up_carts.shape[0] num_dn = old_r_dn_carts.shape[0] @@ -1558,24 +1575,30 @@ def _compute_ratio_determinant_part_rank1_update( # lambda split lambda_matrix_paired, lambda_matrix_unpaired = jnp.split( - geminal_data.lambda_matrix, indices_or_sections=[geminal_data.orb_num_dn], axis=1 + geminal_data._lambda_matrix_jnp, indices_or_sections=[geminal_data.orb_num_dn], axis=1 ) - lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) - lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) - # Precompute old AO matrices once. - orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts) - orb_matrix_dn_old = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, old_r_dn_carts) + # Precompute old AO matrices once. Explicitly upcast to the geminal zone + # (compute_orb_api may return orb_eval dtype, e.g. fp32 for AGP) to avoid + # relying on JAX implicit type promotion in the lambda matmuls below. + orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts).astype(dtype_jnp) + orb_matrix_dn_old = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, old_r_dn_carts).astype(dtype_jnp) # Batched AO for moved electrons (up) -> rows - orb_up_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_new_flat) # (n_orb_up, G) + orb_up_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_new_flat).astype( + dtype_jnp + ) # (n_orb_up, G) tmp_up = jnp.dot(orb_up_new_batch.T, lambda_matrix_paired) # (G, n_orb_dn) row_paired = jnp.dot(tmp_up, orb_matrix_dn_old) # (G, N_dn) row_unpaired = jnp.dot(orb_up_new_batch.T, lambda_matrix_unpaired) # (G, num_unpaired) new_rows_up = jnp.hstack([row_paired, row_unpaired]) # (G, N_up) # Batched AO for moved electrons (dn) -> columns - orb_dn_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_new_flat) # (n_orb_dn, G) + orb_dn_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_new_flat).astype( + dtype_jnp + ) # (n_orb_dn, G) w_batch = jnp.dot(lambda_matrix_paired, orb_dn_new_batch) # (n_orb_up, G) cols = jnp.dot(orb_matrix_up_old.T, w_batch) # (N_up, G) new_cols_dn = cols.T # (G, N_up) @@ -1583,10 +1606,12 @@ def _compute_ratio_determinant_part_rank1_update( # rank-1 determinant ratios for up-move grids and dn-move grids. # Use matrix-matrix contractions first to maximize BLAS/TensorCore utilization, # then extract the moved-electron component per grid. - up_all_cols = jnp.dot(new_rows_up, A_old_inv) # (N_grid, N_up) + # Cast A_old_inv to the det_ratio zone at the use site (Principle 3b). + A_old_inv_z = A_old_inv.astype(dtype_jnp) + up_all_cols = jnp.dot(new_rows_up, A_old_inv_z) # (N_grid, N_up) det_ratio_up = jnp.take_along_axis(up_all_cols, idx_up[:, None], axis=1).reshape(-1) - dn_all_rows = jnp.dot(A_old_inv, new_cols_dn.T).T # (N_grid, N_up) + dn_all_rows = jnp.dot(A_old_inv_z, new_cols_dn.T).T # (N_grid, N_up) det_ratio_dn = jnp.take_along_axis(dn_all_rows, idx_dn[:, None], axis=1).reshape(-1) # Select per grid based on which spin moved. @@ -1632,12 +1657,11 @@ def _compute_ratio_determinant_part_split_spin( exclusively for the block-structured non-local ECP grids produced by the MCMC loop. """ - dtype = get_dtype("geminal") - A_old_inv = jnp.asarray(A_old_inv, dtype=dtype) - old_r_up_carts = jnp.asarray(old_r_up_carts, dtype=dtype) - old_r_dn_carts = jnp.asarray(old_r_dn_carts, dtype=dtype) - new_r_up_shifted = jnp.asarray(new_r_up_shifted, dtype=dtype) - new_r_dn_shifted = jnp.asarray(new_r_dn_shifted, dtype=dtype) + # Forward A_old_inv and old/new r_up/dn coords as-is (Principle 3a — no + # parameter rebind). Module-level forwards (compute_orb_api, + # _compute_ratio_determinant_part_rank1_update) handle their own use-site + # casts. A_old_inv is cast at the use site (Principle 3b) below. + dtype_jnp = get_dtype_jnp("det_ratio") num_up = old_r_up_carts.shape[0] num_dn = old_r_dn_carts.shape[0] @@ -1656,30 +1680,35 @@ def _compute_ratio_determinant_part_split_spin( ) lambda_matrix_paired, lambda_matrix_unpaired = jnp.split( - geminal_data.lambda_matrix, indices_or_sections=[geminal_data.orb_num_dn], axis=1 + geminal_data._lambda_matrix_jnp, indices_or_sections=[geminal_data.orb_num_dn], axis=1 ) - lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) - lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) - # Precompute old AO matrices once. - orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts) - orb_matrix_dn_old = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, old_r_dn_carts) + # Precompute old AO matrices once. Explicitly upcast to the geminal zone + # (compute_orb_api may return orb_eval dtype, e.g. fp32 for AGP) to avoid + # relying on JAX implicit type promotion in the lambda matmuls below. + orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts).astype(dtype_jnp) + orb_matrix_dn_old = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, old_r_dn_carts).astype(dtype_jnp) # ── UP BLOCK: up electron moved, dn unchanged ────────────────────────────── - g_up = new_r_up_shifted.shape[0] delta_up = new_r_up_shifted - old_r_up_carts # (G_up, N_up, 3) moved_up_mask = jnp.any(delta_up != 0.0, axis=2) # (G_up, N_up) idx_up = jnp.argmax(moved_up_mask.astype(jnp.int32), axis=1) # (G_up,) r_up_new_flat = jnp.take_along_axis(new_r_up_shifted, idx_up[:, None, None], axis=1).reshape(-1, 3) # Only evaluate up-spin MOs for the moved electron positions. - orb_up_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_new_flat) # (n_orb_up, G_up) + orb_up_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_new_flat).astype( + dtype_jnp + ) # (n_orb_up, G_up) tmp_up = jnp.dot(orb_up_new_batch.T, lambda_matrix_paired) # (G_up, n_orb_dn) row_paired = jnp.dot(tmp_up, orb_matrix_dn_old) # (G_up, N_dn) row_unpaired = jnp.dot(orb_up_new_batch.T, lambda_matrix_unpaired) # (G_up, num_unpaired) new_rows_up = jnp.hstack([row_paired, row_unpaired]) # (G_up, N_up) - A_col_for_up = jnp.take(A_old_inv, idx_up, axis=1).T # (G_up, N_up) + # Cast A_old_inv to the det_ratio zone at the use site (Principle 3b). + A_old_inv_z = A_old_inv.astype(dtype_jnp) + A_col_for_up = jnp.take(A_old_inv_z, idx_up, axis=1).T # (G_up, N_up) det_ratio_up_block = jnp.sum(new_rows_up * A_col_for_up, axis=1) # (G_up,) # ── DN BLOCK: dn electron moved, up unchanged ────────────────────────────── @@ -1689,11 +1718,13 @@ def _compute_ratio_determinant_part_split_spin( r_dn_new_flat = jnp.take_along_axis(new_r_dn_shifted, idx_dn[:, None, None], axis=1).reshape(-1, 3) # Only evaluate dn-spin MOs for the moved electron positions. - orb_dn_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_new_flat) # (n_orb_dn, G_dn) + orb_dn_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_new_flat).astype( + dtype_jnp + ) # (n_orb_dn, G_dn) w_batch = jnp.dot(lambda_matrix_paired, orb_dn_new_batch) # (n_orb_up, G_dn) new_cols_dn = jnp.dot(orb_matrix_up_old.T, w_batch).T # (G_dn, N_up) - A_row_for_dn = jnp.take(A_old_inv, idx_dn, axis=0) # (G_dn, N_up) + A_row_for_dn = jnp.take(A_old_inv_z, idx_dn, axis=0) # (G_dn, N_up) det_ratio_dn_block = jnp.sum(A_row_for_dn * new_cols_dn, axis=1) # (G_dn,) return jnp.concatenate([det_ratio_up_block, det_ratio_dn_block]) @@ -1707,8 +1738,7 @@ def _compute_ratio_determinant_part_debug( new_r_dn_carts_arr: npt.NDArray[np.float64], ) -> npt.NDArray: """See _api method.""" - dtype = get_dtype("determinant") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("det_ratio") old_r_up_carts = np.asarray(old_r_up_carts, dtype=dtype_np) old_r_dn_carts = np.asarray(old_r_dn_carts, dtype=dtype_np) new_r_up_carts_arr = np.asarray(new_r_up_carts_arr, dtype=dtype_np) @@ -1789,28 +1819,34 @@ def compute_grads_and_laplacian_ln_Det( - Laplacians for spin-up electrons with shape ``(N_up,)``. - Laplacians for spin-down electrons with shape ``(N_dn,)``. """ - dtype = get_dtype("kinetic") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("det_grad_lap") + # NOTE: do not pre-cast r_*_carts. They are forwarded to + # ``compute_geminal_all_elements`` and ``compute_orb_*_api``; the AO + # kernels reconstruct ``r - R`` in float64 internally and a wrapper-level + # downcast would defeat that guard. Arithmetic in this function uses + # ``ao_matrix_*`` / ``lambda_matrix_*`` (cast at their own use sites). # Compute G_inv via SVD pseudoinverse (numerically stable, avoids LU NaN). G = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) - G = jnp.asarray(G, dtype=dtype) + G = jnp.asarray(G, dtype=dtype_jnp) _U, _s, _Vt = jnp.linalg.svd(G, full_matrices=False) # Use conservative threshold to prevent G^{-2} and G^{-3} terms in the # backward pass from diverging. Standard numpy.linalg.pinv uses max(M,N)*eps, # but for de_L/dc (which involves G_inv^2 in the chain rule) we need a larger # safety margin to avoid Inf/NaN in the gradient. EPS_rcond_SVD is set in setting.py # to handle near-singular G while preserving well-conditioned singular values. - eps_rcond = get_eps("rcond_svd", dtype) - _s_inv = jnp.where(_s > eps_rcond * _s[0], jnp.asarray(1.0, dtype=dtype) / _s, jnp.asarray(0.0, dtype=dtype)) + eps_rcond = get_eps("rcond_svd", dtype_jnp) + _s_inv = jnp.where(_s > eps_rcond * _s[0], jnp.asarray(1.0, dtype=dtype_jnp) / _s, jnp.asarray(0.0, dtype=dtype_jnp)) geminal_inverse = (_Vt.T * _s_inv[jnp.newaxis, :]) @ _U.T - lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data.lambda_matrix, [geminal_data.orb_num_dn]) - lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) - lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) + lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) - ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) - ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) + # Explicitly upcast AO/MO forward values to the kinetic zone + # (compute_orb_api may return orb_eval dtype, e.g. fp32 for AGP) to avoid + # relying on JAX implicit type promotion in the lambda/gradient matmuls below. + ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) + ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z = geminal_data.compute_orb_grad_api( geminal_data.orb_data_up_spin, r_up_carts @@ -1834,7 +1870,7 @@ def compute_grads_and_laplacian_ln_Det( geminal_grad_dn_paired = jnp.einsum("ia,gaj->gij", ao_matrix_up.T, paired_dn_grads) geminal_grad_dn_unpaired = jnp.zeros( (3, geminal_data.num_electron_up, geminal_data.num_electron_up - geminal_data.num_electron_dn), - dtype=geminal_grad_dn_paired.dtype, + dtype=dtype_jnp, ) geminal_grad_dn = jnp.concatenate([geminal_grad_dn_paired, geminal_grad_dn_unpaired], axis=2) @@ -1845,7 +1881,7 @@ def compute_grads_and_laplacian_ln_Det( geminal_laplacian_dn_paired = jnp.dot(ao_matrix_up.T, jnp.dot(lambda_matrix_paired, ao_matrix_laplacian_dn)) geminal_laplacian_dn_unpaired = jnp.zeros( [geminal_data.num_electron_up, geminal_data.num_electron_up - geminal_data.num_electron_dn], - dtype=geminal_laplacian_dn_paired.dtype, + dtype=dtype_jnp, ) geminal_laplacian_dn = jnp.hstack([geminal_laplacian_dn_paired, geminal_laplacian_dn_unpaired]) @@ -1887,16 +1923,19 @@ def _grads_lap_body( passed to ``jax.vjp`` inside the custom VJP backward pass without creating a dependency on the public fast function. """ - dtype = get_dtype("kinetic") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - geminal_inverse = jnp.asarray(geminal_inverse, dtype=dtype) - lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data.lambda_matrix, [geminal_data.orb_num_dn]) - lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) - lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) - - ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) - ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) + dtype_jnp = get_dtype_jnp("det_grad_lap") + # r_*_carts are only forwarded; do not pre-cast (see the public function + # ``compute_grads_and_laplacian_ln_Det`` for the rationale). + geminal_inverse = jnp.asarray(geminal_inverse, dtype=dtype_jnp) + lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) + + # Explicitly upcast AO/MO forward values to the kinetic zone + # (compute_orb_api may return orb_eval dtype, e.g. fp32 for AGP) to avoid + # relying on JAX implicit type promotion in the lambda/gradient matmuls below. + ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) + ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z = geminal_data.compute_orb_grad_api( geminal_data.orb_data_up_spin, r_up_carts @@ -1920,7 +1959,7 @@ def _grads_lap_body( geminal_grad_dn_paired = jnp.einsum("ia,gaj->gij", ao_matrix_up.T, paired_dn_grads) geminal_grad_dn_unpaired = jnp.zeros( (3, geminal_data.num_electron_up, geminal_data.num_electron_up - geminal_data.num_electron_dn), - dtype=geminal_grad_dn_paired.dtype, + dtype=dtype_jnp, ) geminal_grad_dn = jnp.concatenate([geminal_grad_dn_paired, geminal_grad_dn_unpaired], axis=2) @@ -1931,7 +1970,7 @@ def _grads_lap_body( geminal_laplacian_dn_paired = jnp.dot(ao_matrix_up.T, jnp.dot(lambda_matrix_paired, ao_matrix_laplacian_dn)) geminal_laplacian_dn_unpaired = jnp.zeros( [geminal_data.num_electron_up, geminal_data.num_electron_up - geminal_data.num_electron_dn], - dtype=geminal_laplacian_dn_paired.dtype, + dtype=dtype_jnp, ) geminal_laplacian_dn = jnp.hstack([geminal_laplacian_dn_paired, geminal_laplacian_dn_unpaired]) @@ -1966,15 +2005,15 @@ def _grads_lap_fwd( r_dn_carts: jax.Array, ): """Forward pass: compute stable G_inv and primal outputs.""" - dtype = get_dtype("kinetic") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("det_grad_lap") + # r_*_carts are only forwarded; do not pre-cast (see + # ``compute_grads_and_laplacian_ln_Det`` for the rationale). G = compute_geminal_all_elements(geminal_data, r_up_carts, r_dn_carts) - G = jnp.asarray(G, dtype=dtype) + G = jnp.asarray(G, dtype=dtype_jnp) _U, _s, _Vt = jnp.linalg.svd(G, full_matrices=False) # Use same conservative threshold as in compute_grads_and_laplacian_ln_Det - eps_rcond = get_eps("rcond_svd", dtype) - _s_inv = jnp.where(_s > eps_rcond * _s[0], jnp.asarray(1.0, dtype=dtype) / _s, jnp.asarray(0.0, dtype=dtype)) + eps_rcond = get_eps("rcond_svd", dtype_jnp) + _s_inv = jnp.where(_s > eps_rcond * _s[0], jnp.asarray(1.0, dtype=dtype_jnp) / _s, jnp.asarray(0.0, dtype=dtype_jnp)) G_inv_stable = (_Vt.T * _s_inv[jnp.newaxis, :]) @ _U.T primals = _grads_lap_body(geminal_data, r_up_carts, r_dn_carts, G_inv_stable) return primals, (geminal_data, r_up_carts, r_dn_carts, G_inv_stable) @@ -2006,9 +2045,9 @@ def _grads_lap_bwd(res, g): :func:`compute_grads_and_laplacian_ln_Det` for details). Keep ``EPS_rcond_SVD`` very small (e.g. ``1e-20``) to avoid this. """ - dtype = get_dtype("kinetic") + dtype_jnp = get_dtype_jnp("det_grad_lap") geminal_data, r_up_carts, r_dn_carts, G_inv_stable = res - G_inv_stable = jnp.asarray(G_inv_stable, dtype=dtype) + G_inv_stable = jnp.asarray(G_inv_stable, dtype=dtype_jnp) # Step 1: differentiate _grads_lap_body w.r.t. all args. # This gives direct gradients (AO path) and G_inv_bar (cotangent for G_inv). @@ -2030,8 +2069,12 @@ def _grads_lap_bwd(res, g): # determined by (r_up_carts, r_dn_carts) (kinetic zone). In mixed precision the # G_inv_bar / G_inv_stable arithmetic can promote ``G_bar_from_inv`` to a wider # dtype, so cast back to the primal output dtype before invoking ``vjp_fn2``. - geminal_primal_out, vjp_fn2 = jax.vjp(compute_geminal_all_elements, geminal_data, r_up_carts, r_dn_carts) - G_bar_from_inv = jnp.asarray(G_bar_from_inv, dtype=geminal_primal_out.dtype) + # The vjp_fn2 cotangent dtype must match the primal output dtype of + # compute_geminal_all_elements (det_eval zone). Use get_dtype_jnp("det_eval") + # explicitly rather than borrowing geminal_primal_out.dtype, to keep the + # cast target source-visible per the consumer-zone principle. + _, vjp_fn2 = jax.vjp(compute_geminal_all_elements, geminal_data, r_up_carts, r_dn_carts) + G_bar_from_inv = jnp.asarray(G_bar_from_inv, dtype=get_dtype_jnp("det_eval")) d_geminal_inv, d_r_up_inv, d_r_dn_inv = vjp_fn2(G_bar_from_inv) # Total: sum both contributions. @@ -2077,17 +2120,20 @@ def compute_grads_and_laplacian_ln_Det_fast( if geminal_inverse is None: raise ValueError("geminal_inverse must be provided for fast evaluation") - dtype = get_dtype("kinetic") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - geminal_inverse = jnp.asarray(geminal_inverse, dtype=dtype) + dtype_jnp = get_dtype_jnp("det_grad_lap") + # r_*_carts and geminal_inverse are only forwarded; do not pre-cast + # (Principle 3a — no parameter rebind). geminal_inverse is cast to the + # det_grad_lap zone at each einsum use site below (Principle 3b). - lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data.lambda_matrix, [geminal_data.orb_num_dn]) - lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype) - lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype) + lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) - ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts) - ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts) + # Explicitly upcast AO/MO forward values to the kinetic zone + # (compute_orb_api may return orb_eval dtype, e.g. fp32 for AGP) to avoid + # relying on JAX implicit type promotion in the lambda/gradient matmuls below. + ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) + ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z = geminal_data.compute_orb_grad_api( geminal_data.orb_data_up_spin, r_up_carts @@ -2111,7 +2157,7 @@ def compute_grads_and_laplacian_ln_Det_fast( geminal_grad_dn_paired = jnp.einsum("ia,gaj->gij", ao_matrix_up.T, paired_dn_grads) geminal_grad_dn_unpaired = jnp.zeros( (3, geminal_data.num_electron_up, geminal_data.num_electron_up - geminal_data.num_electron_dn), - dtype=geminal_grad_dn_paired.dtype, + dtype=dtype_jnp, ) geminal_grad_dn = jnp.concatenate([geminal_grad_dn_paired, geminal_grad_dn_unpaired], axis=2) @@ -2122,12 +2168,14 @@ def compute_grads_and_laplacian_ln_Det_fast( geminal_laplacian_dn_paired = jnp.dot(ao_matrix_up.T, jnp.dot(lambda_matrix_paired, ao_matrix_laplacian_dn)) geminal_laplacian_dn_unpaired = jnp.zeros( [geminal_data.num_electron_up, geminal_data.num_electron_up - geminal_data.num_electron_dn], - dtype=geminal_laplacian_dn_paired.dtype, + dtype=dtype_jnp, ) geminal_laplacian_dn = jnp.hstack([geminal_laplacian_dn_paired, geminal_laplacian_dn_unpaired]) - grad_ln_D_up_stack = jnp.einsum("gij,ji->gi", geminal_grad_up, geminal_inverse) - grad_ln_D_dn_stack = jnp.einsum("ij,gji->gi", geminal_inverse, geminal_grad_dn) + # Cast geminal_inverse to the det_grad_lap zone at the use site (Principle 3b). + G_inv = geminal_inverse.astype(dtype_jnp) + grad_ln_D_up_stack = jnp.einsum("gij,ji->gi", geminal_grad_up, G_inv) + grad_ln_D_dn_stack = jnp.einsum("ij,gji->gi", G_inv, geminal_grad_dn) grad_ln_D_up = grad_ln_D_up_stack.T grad_ln_D_dn = grad_ln_D_dn_stack.T @@ -2137,11 +2185,11 @@ def compute_grads_and_laplacian_ln_Det_fast( lap_ln_D_up = -( grad_ln_D_up_x * grad_ln_D_up_x + grad_ln_D_up_y * grad_ln_D_up_y + grad_ln_D_up_z * grad_ln_D_up_z - ) + jnp.einsum("ij,ji->i", geminal_laplacian_up, geminal_inverse) + ) + jnp.einsum("ij,ji->i", geminal_laplacian_up, G_inv) lap_ln_D_dn = -( grad_ln_D_dn_x * grad_ln_D_dn_x + grad_ln_D_dn_y * grad_ln_D_dn_y + grad_ln_D_dn_z * grad_ln_D_dn_z - ) + jnp.einsum("ij,ji->i", geminal_inverse, geminal_laplacian_dn) + ) + jnp.einsum("ij,ji->i", G_inv, geminal_laplacian_dn) # Trim to n_dn for open-shell (N_up > N_dn) systems n_dn = geminal_data.num_electron_dn @@ -2157,9 +2205,8 @@ def _compute_grads_and_laplacian_ln_Det_fast_debug( r_dn_carts: jax.Array, ) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]: """Debug helper that uses auto-diff to validate the fast path.""" - dtype = get_dtype("kinetic") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + # Pure delegation to ``_compute_grads_and_laplacian_ln_Det_auto``; + # no cast needed at this wrapper. # Use auto-diff as the reference (independent implementation). grad_ln_D_up, grad_ln_D_dn, lap_ln_D_up, lap_ln_D_dn = _compute_grads_and_laplacian_ln_Det_auto( geminal_data=geminal_data, @@ -2186,24 +2233,27 @@ def _compute_grads_and_laplacian_ln_Det_auto( Uses autodiff on ln|det(G)| to compute gradients w.r.t. electron positions and per-electron Laplacians. """ - dtype = get_dtype("kinetic") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). Cast + # to the det_grad_lap zone at the use site before passing as the + # differentiation operand to grad/jacfwd (Principle 3b). + dtype_jnp = get_dtype_jnp("det_grad_lap") + r_up_z = r_up_carts.astype(dtype_jnp) + r_dn_z = r_dn_carts.astype(dtype_jnp) def ln_det_fn(r_up, r_dn): return compute_ln_det_geminal_all_elements(geminal_data, r_up, r_dn) - grad_ln_D_up = jax.grad(ln_det_fn, argnums=0)(r_up_carts, r_dn_carts) - grad_ln_D_dn = jax.grad(ln_det_fn, argnums=1)(r_up_carts, r_dn_carts) + grad_ln_D_up = jax.grad(ln_det_fn, argnums=0)(r_up_z, r_dn_z) + grad_ln_D_dn = jax.grad(ln_det_fn, argnums=1)(r_up_z, r_dn_z) def grad_up_fn(r_up): - return jax.grad(ln_det_fn, argnums=0)(r_up, r_dn_carts) + return jax.grad(ln_det_fn, argnums=0)(r_up, r_dn_z) def grad_dn_fn(r_dn): - return jax.grad(ln_det_fn, argnums=1)(r_up_carts, r_dn) + return jax.grad(ln_det_fn, argnums=1)(r_up_z, r_dn) - jac_up = jax.jacfwd(grad_up_fn)(r_up_carts) - jac_dn = jax.jacfwd(grad_dn_fn)(r_dn_carts) + jac_up = jax.jacfwd(grad_up_fn)(r_up_z) + jac_dn = jax.jacfwd(grad_dn_fn)(r_dn_z) laplacian_ln_D_up = jnp.einsum("ijij->i", jac_up) laplacian_ln_D_dn = jnp.einsum("ijij->i", jac_dn) @@ -2222,8 +2272,7 @@ def _compute_grads_and_laplacian_ln_Det_debug( np.ndarray, ]: """See compute_grads_and_laplacian_ln_Det_api.""" - dtype = get_dtype("kinetic") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("det_grad_lap") r_up_carts = np.asarray(r_up_carts, dtype=dtype_np) r_dn_carts = np.asarray(r_dn_carts, dtype=dtype_np) det_geminal = compute_det_geminal_all_elements( diff --git a/jqmc/hamiltonians.py b/jqmc/hamiltonians.py index ceaccd25..7104225b 100644 --- a/jqmc/hamiltonians.py +++ b/jqmc/hamiltonians.py @@ -56,7 +56,7 @@ from jax import jit from jax import typing as jnpt -from ._precision import get_dtype +from ._precision import get_dtype_jnp from .coulomb_potential import Coulomb_potential_data, compute_coulomb_potential, compute_coulomb_potential_fast from .structure import Structure_data from .wavefunction import ( @@ -199,9 +199,10 @@ def compute_local_energy( Returns: float: The value of local energy (e_L) with the given wavefunction (float) """ - dtype = get_dtype("kinetic") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("local_energy") + # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). Each + # downstream consumer (compute_kinetic_energy, compute_coulomb_potential) + # casts to its own zone at the use site. T = compute_kinetic_energy( wavefunction_data=hamiltonian_data.wavefunction_data, @@ -217,7 +218,8 @@ def compute_local_energy( wavefunction_data=hamiltonian_data.wavefunction_data, ) - return jnp.asarray(T, dtype=dtype) + jnp.asarray(V, dtype=dtype) + # Cast scalar zone outputs to local_energy zone at the sum (Principle 3b). + return T.astype(dtype_jnp) + V.astype(dtype_jnp) def compute_local_energy_fast( @@ -263,9 +265,9 @@ def compute_local_energy_fast( Passing an inverse from a different configuration silently produces incorrect kinetic energy. """ - dtype = get_dtype("kinetic") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("local_energy") + # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). Each + # downstream consumer casts to its own zone at the use site. T_up_elements, T_dn_elements = compute_kinetic_energy_all_elements_fast_update( wavefunction_data=hamiltonian_data.wavefunction_data, @@ -284,7 +286,8 @@ def compute_local_energy_fast( wavefunction_data=hamiltonian_data.wavefunction_data, ) - return jnp.asarray(T, dtype=dtype) + jnp.asarray(V, dtype=dtype) + # Cast scalar zone outputs to local_energy zone at the sum (Principle 3b). + return T.astype(dtype_jnp) + V.astype(dtype_jnp) @jit @@ -311,9 +314,9 @@ def _compute_local_energy_auto( Returns: float: The value of local energy (e_L) with the given wavefunction (float) """ - dtype = get_dtype("kinetic") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("local_energy") + # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). Each + # downstream consumer casts to its own zone at the use site. T = _compute_kinetic_energy_auto( wavefunction_data=hamiltonian_data.wavefunction_data, @@ -329,7 +332,8 @@ def _compute_local_energy_auto( wavefunction_data=hamiltonian_data.wavefunction_data, ) - return jnp.asarray(T, dtype=dtype) + jnp.asarray(V, dtype=dtype) + # Cast scalar zone outputs to local_energy zone at the sum (Principle 3b). + return T.astype(dtype_jnp) + V.astype(dtype_jnp) def _reconstruct_dataclass(cls, obj): @@ -508,15 +512,19 @@ def _load_dataclass_from_hdf5(cls: Type[T], group: h5py.Group) -> T: elif isinstance(val, list) and (field.type is tuple or "tuple" in str(field.type)): val = tuple(val) - # Convert np.ndarray or list/tuple to jax.Array for fields typed as jax.Array + # Convert np.ndarray or list/tuple to jax.Array for fields typed as jax.Array. + # Note: fields typed `npt.NDArray[np.float64]` (string-form annotation) must + # NOT trigger this branch — they are stored as numpy arrays. Exclude both + # "ndarray" (resolved form) and "NDArray" (npt alias form). if ( isinstance(val, (np.ndarray, list, tuple)) and "Array" in str(field.type) and "ndarray" not in str(field.type) + and "NDArray" not in str(field.type) and "list" not in str(field.type) and "tuple" not in str(field.type) ): - val = jnp.asarray(val, dtype=get_dtype("io")) + val = jnp.asarray(val, dtype=jnp.float64) init_args[field.name] = val elif field.name in group.attrs: diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 76a29681..605c1bc0 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -61,7 +61,7 @@ from jax import typing as jnpt from jax.tree_util import tree_flatten, tree_unflatten -from ._precision import get_dtype +from ._precision import get_dtype_jnp, get_dtype_np from ._setting import EPS_safe_distance, atol_consistency from .atomic_orbital import ( AOs_cart_data, @@ -433,12 +433,14 @@ def _pairwise_distances(self, A: jnp.ndarray, B: jnp.ndarray) -> jnp.ndarray: jnp.ndarray: ``(n_a, n_b)`` matrix with a small epsilon added before the square root to keep gradients finite when particles coincide. """ - dtype_j = get_dtype("jastrow") + dtype_jnp = get_dtype_jnp("jastrow_eval") if A.shape[0] == 0 or B.shape[0] == 0: - return jnp.zeros((A.shape[0], B.shape[0]), dtype=dtype_j) - # Compute differences in float64 to avoid catastrophic cancellation - # under float32 jastrow zone, then downcast. - diff = (A.astype(jnp.float64)[:, None, :] - B.astype(jnp.float64)[None, :, :]).astype(dtype_j) + return jnp.zeros((A.shape[0], B.shape[0]), dtype=dtype_jnp) + # Reconstruct differences in caller-supplied precision (fp64 from MCMC + # walker state) via JAX promotion when one operand is fp64, then downcast + # to the jastrow_eval zone. Avoids catastrophic cancellation without + # hardcoding fp64. + diff = (A[:, None, :] - B[None, :, :]).astype(dtype_jnp) return jnp.sqrt(jnp.sum(diff**2, axis=-1) + EPS_safe_distance) def _nuclear_embeddings(self, Z_n: jnp.ndarray) -> jnp.ndarray: @@ -451,10 +453,10 @@ def _nuclear_embeddings(self, Z_n: jnp.ndarray) -> jnp.ndarray: jnp.ndarray: ``(n_nuc, hidden_dim)`` embeddings looked up through ``species_lookup``. Returns an empty array when no nuclei are present. """ - dtype = get_dtype("jastrow") + dtype_jnp = get_dtype_jnp("jastrow_eval") n_nuc = Z_n.shape[0] if n_nuc == 0: - return jnp.zeros((0, self.hidden_dim), dtype=dtype) + return jnp.zeros((0, self.hidden_dim), dtype=dtype_jnp) lookup = jnp.asarray(self.species_lookup) species_ids = jnp.take(lookup, Z_n.astype(jnp.int32), mode="clip") @@ -513,10 +515,9 @@ def __call__( The network is permutation equivariant within each spin channel and rotation invariant by construction of the PhysNet radial features. """ - dtype = get_dtype("jastrow") - r_up = jnp.asarray(r_up, dtype=dtype) - r_dn = jnp.asarray(r_dn, dtype=dtype) - R_n = jnp.asarray(R_n, dtype=dtype) + # Forward r_up/r_dn/R_n as-is (Principle 3a — no parameter rebind). + # `_pairwise_distances` reconstructs the differences in caller-supplied + # precision and downcasts to the jastrow_eval zone at the use site. Z_n = jnp.asarray(Z_n) n_up = r_up.shape[0] @@ -627,8 +628,7 @@ def _logger_info(self) -> None: @classmethod def init_jastrow_one_body_data(cls, jastrow_1b_param, structure_data, core_electrons, jastrow_1b_type="exp"): """Initialization.""" - dtype = get_dtype("jastrow") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_eval") jastrow_one_body_data = cls( jastrow_1b_param=np.asarray(jastrow_1b_param, dtype=dtype_np).reshape(()), jastrow_1b_type=jastrow_1b_type, @@ -657,15 +657,16 @@ def compute_Jastrow_one_body( Returns: float: One-body Jastrow value (before exponentiation). """ - dtype = get_dtype("jastrow") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + # NOTE: do not pre-cast r_*_carts. ``one_body_jastrow_kernel`` reconstructs + # ``r - R`` in float64 internally to avoid catastrophic cancellation; + # a wrapper-level downcast would defeat that guard. + dtype_jnp = get_dtype_jnp("jastrow_eval") # Retrieve structure data and convert to JAX arrays - R_carts = jnp.array(jastrow_one_body_data.structure_data.positions, dtype=dtype) - atomic_numbers = jnp.array(jastrow_one_body_data.structure_data.atomic_numbers, dtype=dtype) - core_electrons = jnp.array(jastrow_one_body_data.core_electrons, dtype=dtype) + R_carts = jastrow_one_body_data.structure_data._positions_cart_jnp.astype(dtype_jnp) + atomic_numbers = jnp.array(jastrow_one_body_data.structure_data.atomic_numbers, dtype=dtype_jnp) + core_electrons = jnp.array(jastrow_one_body_data.core_electrons, dtype=dtype_jnp) effective_charges = atomic_numbers - core_electrons - j1b = jnp.asarray(jastrow_one_body_data.jastrow_1b_param, dtype=dtype) + j1b = jnp.asarray(jastrow_one_body_data.jastrow_1b_param, dtype=dtype_jnp) j1b_type = jastrow_one_body_data.jastrow_1b_type @@ -678,8 +679,11 @@ def one_body_jastrow_kernel( R_cart: jnpt.ArrayLike, ) -> float: """Exponential form of J1.""" - # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. - diff = (r_cart.astype(jnp.float64) - R_cart.astype(jnp.float64)).astype(r_cart.dtype) + # Reconstruct r - R in caller-supplied precision (fp64 from MCMC walker + # state) via JAX promotion when one operand is fp64, then downcast to + # the jastrow_eval zone. Avoids catastrophic cancellation without + # hardcoding fp64. + diff = (r_cart - R_cart).astype(dtype_jnp) return 1.0 / (2.0 * param) * (1.0 - jnp.exp(-param * coeff * jnp.linalg.norm(diff))) def atom_contrib(r_cart, R_cart, Z_eff): @@ -690,8 +694,11 @@ def atom_contrib(r_cart, R_cart, Z_eff): def atom_contrib(r_cart, R_cart, Z_eff): """Pade form of J1: -Z_eff^{3/4} * r_eN / (2*(1 + a * Z_eff^{1/4} * r_eN)).""" - # Compute r-R in float64 to avoid catastrophic cancellation under float32 zones. - diff = (r_cart.astype(jnp.float64) - R_cart.astype(jnp.float64)).astype(r_cart.dtype) + # Reconstruct r - R in caller-supplied precision (fp64 from MCMC walker + # state) via JAX promotion when one operand is fp64, then downcast to + # the jastrow_eval zone. Avoids catastrophic cancellation without + # hardcoding fp64. + diff = (r_cart - R_cart).astype(dtype_jnp) r_eN = jnp.linalg.norm(diff) coeff = (2.0 * Z_eff) ** (1.0 / 4.0) return -((2.0 * Z_eff) ** (3.0 / 4.0)) * r_eN / (2.0 * (1.0 + j1b * coeff * r_eN)) @@ -718,8 +725,7 @@ def _compute_Jastrow_one_body_debug( r_dn_carts: npt.NDArray[np.float64], ) -> float: """See compute_Jastrow_one_body_api.""" - dtype = get_dtype("jastrow") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_eval") positions = jastrow_one_body_data.structure_data.positions atomic_numbers = jastrow_one_body_data.structure_data.atomic_numbers core_electrons = jastrow_one_body_data.core_electrons @@ -775,8 +781,7 @@ def _compute_grads_and_laplacian_Jastrow_one_body_debug( np.ndarray, ]: """Numerical gradients and Laplacian for one-body Jastrow (debug).""" - dtype = get_dtype("kinetic") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_grad_lap") diff_h = 1.0e-5 r_up_carts = np.array(r_up_carts, dtype=dtype_np) r_dn_carts = np.array(r_dn_carts, dtype=dtype_np) @@ -929,9 +934,9 @@ def _compute_grads_and_laplacian_Jastrow_one_body_auto( jax.Array, ]: """Auto-diff gradients and Laplacian for one-body Jastrow.""" - dtype = get_dtype("kinetic") - r_up_carts = jnp.array(r_up_carts, dtype=dtype) - r_dn_carts = jnp.array(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + r_up_carts = jnp.array(r_up_carts, dtype=dtype_jnp) + r_dn_carts = jnp.array(r_dn_carts, dtype=dtype_jnp) grad_J1_up = grad(compute_Jastrow_one_body, argnums=1)(jastrow_one_body_data, r_up_carts, r_dn_carts) grad_J1_dn = grad(compute_Jastrow_one_body, argnums=2)(jastrow_one_body_data, r_up_carts, r_dn_carts) @@ -963,10 +968,10 @@ def compute_grads_and_laplacian_Jastrow_one_body( Gradients for up/down electrons with shapes ``(N_up, 3)`` and ``(N_dn, 3)``, Laplacians for up/down electrons with shapes ``(N_up,)`` and ``(N_dn,)``. """ - dtype = get_dtype("kinetic") - positions = jnp.asarray(jastrow_one_body_data.structure_data.positions, dtype=dtype) - atomic_numbers = jnp.asarray(jastrow_one_body_data.structure_data.atomic_numbers) - core_electrons = jnp.asarray(jastrow_one_body_data.core_electrons) + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + positions = jastrow_one_body_data.structure_data._positions_cart_jnp.astype(dtype_jnp) + atomic_numbers = jnp.asarray(jastrow_one_body_data.structure_data.atomic_numbers, dtype=dtype_jnp) + core_electrons = jnp.asarray(jastrow_one_body_data.core_electrons, dtype=dtype_jnp) z_eff = atomic_numbers - core_electrons a = jastrow_one_body_data.jastrow_1b_param @@ -1016,8 +1021,8 @@ def _grad_lap_one_spin(r_carts): else: raise ValueError(f"Unknown jastrow_1b_type: {j1b_type}") - grad_up, lap_up = _grad_lap_one_spin(jnp.asarray(r_up_carts, dtype=dtype)) - grad_dn, lap_dn = _grad_lap_one_spin(jnp.asarray(r_dn_carts, dtype=dtype)) + grad_up, lap_up = _grad_lap_one_spin(jnp.asarray(r_up_carts, dtype=dtype_jnp)) + grad_dn, lap_dn = _grad_lap_one_spin(jnp.asarray(r_dn_carts, dtype=dtype_jnp)) return grad_up, grad_dn, lap_up, lap_dn @@ -1084,8 +1089,7 @@ def _logger_info(self) -> None: @classmethod def init_jastrow_two_body_data(cls, jastrow_2b_param=1.0, jastrow_2b_type="pade"): """Initialization.""" - dtype = get_dtype("jastrow") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_eval") jastrow_two_body_data = cls( jastrow_2b_param=np.asarray(jastrow_2b_param, dtype=dtype_np).reshape(()), jastrow_2b_type=jastrow_2b_type, @@ -1112,22 +1116,27 @@ def compute_Jastrow_two_body( Returns: float: Two-body Jastrow value (before exponentiation). """ - dtype_j2 = get_dtype("jastrow") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype_j2) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype_j2) - j2b_param = jnp.asarray(jastrow_two_body_data.jastrow_2b_param, dtype=dtype_j2) + # NOTE: do not pre-cast r_*_carts. ``two_body_jastrow_pade``/``_exp`` + # reconstruct ``r_i - r_j`` in float64 internally to avoid catastrophic + # cancellation; a wrapper-level downcast would defeat that guard. + dtype_jnp = get_dtype_jnp("jastrow_eval") + j2b_param = jnp.asarray(jastrow_two_body_data.jastrow_2b_param, dtype=dtype_jnp) j2b_type = jastrow_two_body_data.jastrow_2b_type def two_body_jastrow_exp(param: float, r_cart_i: jnpt.ArrayLike, r_cart_j: jnpt.ArrayLike) -> float: """Exponential form of J2.""" - # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. - diff = (r_cart_i.astype(jnp.float64) - r_cart_j.astype(jnp.float64)).astype(r_cart_i.dtype) + # Reconstruct r_i - r_j in caller-supplied precision (fp64 from MCMC walker + # state) via JAX promotion when one operand is fp64, then downcast to the + # jastrow_eval zone. Avoids catastrophic cancellation without hardcoding fp64. + diff = (r_cart_i - r_cart_j).astype(dtype_jnp) return 1.0 / (2.0 * param) * (1.0 - jnp.exp(-param * jnp.linalg.norm(diff))) def two_body_jastrow_pade(param: float, r_cart_i: jnpt.ArrayLike, r_cart_j: jnpt.ArrayLike) -> float: """Pade form of J2.""" - # Compute r_i - r_j in float64 to avoid catastrophic cancellation under float32 zones. - diff = (r_cart_i.astype(jnp.float64) - r_cart_j.astype(jnp.float64)).astype(r_cart_i.dtype) + # Reconstruct r_i - r_j in caller-supplied precision (fp64 from MCMC walker + # state) via JAX promotion when one operand is fp64, then downcast to the + # jastrow_eval zone. Avoids catastrophic cancellation without hardcoding fp64. + diff = (r_cart_i - r_cart_j).astype(dtype_jnp) r_ij = jnp.linalg.norm(diff) return r_ij / 2.0 * (1.0 + param * r_ij) ** (-1.0) @@ -1235,15 +1244,15 @@ class Jastrow_three_body_data: Args: orb_data (AOs_sphe_data | AOs_cart_data | MOs_data): Basis/orbital data used for both spins. - j_matrix (npt.NDArray | jax.Array): J matrix with shape ``(orb_num, orb_num + 1)``. + j_matrix (npt.NDArray[np.float64]): J matrix with shape ``(orb_num, orb_num + 1)``. dtype: float64. """ orb_data: AOs_sphe_data | AOs_cart_data | MOs_data = struct.field( pytree_node=True, default_factory=AOs_sphe_data ) #: Orbital basis (AOs or MOs) shared across spins. - j_matrix: npt.NDArray | jax.Array = struct.field( - pytree_node=True, default_factory=lambda: np.array([]) - ) #: J3/J1 matrix; square block plus final column. + j_matrix: npt.NDArray[np.float64] = struct.field( + pytree_node=True, default_factory=lambda: np.array([], dtype=np.float64) + ) #: J3/J1 matrix; square block plus final column. dtype: float64. def sanity_check(self) -> None: """Check attributes of the class. @@ -1311,27 +1320,34 @@ def compute_orb_api(self) -> Callable[..., npt.NDArray[np.float64]]: else: raise NotImplementedError + @property + def _j_matrix_jnp(self) -> jax.Array: + """Return j_matrix as a jax.Array (jnp view of the underlying numpy storage).""" + # Lift-only fp64 basis-data storage accessor (see _precision.py exemption); + # consumer casts to its own zone at use site. + return jnp.asarray(self.j_matrix, dtype=jnp.float64) + @property def ao_exponents(self) -> jax.Array: - """AO Gaussian exponents, regardless of AO/MO representation.""" + """AO Gaussian exponents (jnp view of underlying numpy storage).""" if isinstance(self.orb_data, (AOs_sphe_data, AOs_cart_data)): - return self.orb_data.exponents + return self.orb_data._exponents_jnp elif isinstance(self.orb_data, MOs_data): - return self.orb_data.aos_data.exponents + return self.orb_data.aos_data._exponents_jnp else: raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") @property def ao_coefficients(self) -> jax.Array: - """AO contraction coefficients, regardless of AO/MO representation.""" + """AO contraction coefficients (jnp view of underlying numpy storage).""" if isinstance(self.orb_data, (AOs_sphe_data, AOs_cart_data)): - return self.orb_data.coefficients + return self.orb_data._coefficients_jnp elif isinstance(self.orb_data, MOs_data): - return self.orb_data.aos_data.coefficients + return self.orb_data.aos_data._coefficients_jnp else: raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") - def with_updated_ao_exponents(self, new_exp: jax.Array) -> "Jastrow_three_body_data": + def with_updated_ao_exponents(self, new_exp: npt.NDArray[np.float64]) -> "Jastrow_three_body_data": """Return a new instance with updated AO exponents.""" if isinstance(self.orb_data, (AOs_sphe_data, AOs_cart_data)): return self.replace(orb_data=self.orb_data.replace(exponents=new_exp)) @@ -1341,7 +1357,7 @@ def with_updated_ao_exponents(self, new_exp: jax.Array) -> "Jastrow_three_body_d else: raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") - def with_updated_ao_coefficients(self, new_coeff: jax.Array) -> "Jastrow_three_body_data": + def with_updated_ao_coefficients(self, new_coeff: npt.NDArray[np.float64]) -> "Jastrow_three_body_data": """Return a new instance with updated AO contraction coefficients.""" if isinstance(self.orb_data, (AOs_sphe_data, AOs_cart_data)): return self.replace(orb_data=self.orb_data.replace(coefficients=new_coeff)) @@ -1367,8 +1383,7 @@ def init_jastrow_three_body_data( random_scale: Upper bound of uniform sampler when random_init is True (default 0.01). seed: Optional seed for deterministic initialization when random_init is True. """ - dtype = get_dtype("jastrow") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_eval") if random_init: rng = np.random.default_rng(seed) j_matrix = rng.uniform(0.0, random_scale, size=(orb_data._num_orb, orb_data._num_orb + 1)).astype(dtype_np) @@ -1396,8 +1411,7 @@ def to_cartesian(self) -> "Jastrow_three_body_data": aos_cart, transform_matrix = _aos_sphe_to_cart(self.orb_data) - dtype = get_dtype("jastrow") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_eval") square_sph = np.asarray(self.j_matrix[:, :-1], dtype=dtype_np) j1_sph = np.asarray(self.j_matrix[:, -1], dtype=dtype_np) square_cart = transform_matrix.T @ square_sph @ transform_matrix @@ -1424,8 +1438,7 @@ def to_spherical(self) -> "Jastrow_three_body_data": aos_sphe, transform_pinv = _aos_cart_to_sphe(self.orb_data) - dtype = get_dtype("jastrow") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_eval") square_cart = np.asarray(self.j_matrix[:, :-1], dtype=dtype_np) j1_cart = np.asarray(self.j_matrix[:, -1], dtype=dtype_np) square_sph = transform_pinv.T @ square_cart @ transform_pinv @@ -1591,10 +1604,10 @@ def init_from_structure( # Dummy electron positions for parameter initialization: # use one spin-up and one spin-down electron at the origin so that # both PauliNet channels are initialized with valid shapes. - dtype = get_dtype("jastrow") - r_up_init = jnp.zeros((1, 3), dtype=dtype) - r_dn_init = jnp.zeros((1, 3), dtype=dtype) - R_n = jnp.asarray(structure_data.positions, dtype=dtype) # (n_nuc, 3) + dtype_jnp = get_dtype_jnp("jastrow_eval") + r_up_init = jnp.zeros((1, 3), dtype=dtype_jnp) + r_dn_init = jnp.zeros((1, 3), dtype=dtype_jnp) + R_n = structure_data._positions_cart_jnp.astype(dtype_jnp) # (n_nuc, 3) Z_n = jnp.asarray(structure_data.atomic_numbers) # (n_nuc,) rngs = {"params": key} @@ -1667,18 +1680,18 @@ def compute_Jastrow_three_body( Returns: float: Three-body Jastrow value (before exponentiation). """ - dtype = get_dtype("jastrow") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) - j_matrix = jastrow_three_body_data.j_matrix.astype(dtype) + # r_*_carts forwarded unchanged to ``compute_orb_api`` (the AO/MO kernels + # reconstruct ``r - R`` in float64 internally); do not pre-cast here. + dtype_jnp = get_dtype_jnp("jastrow_eval") + j_matrix = jastrow_three_body_data._j_matrix_jnp.astype(dtype_jnp) num_electron_up = len(r_up_carts) num_electron_dn = len(r_dn_carts) - aos_up = jnp.array(jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_up_carts), dtype=dtype) - aos_dn = jnp.array(jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_dn_carts), dtype=dtype) + aos_up = jnp.array(jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_up_carts), dtype=dtype_jnp) + aos_dn = jnp.array(jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_dn_carts), dtype=dtype_jnp) - K_up = jnp.tril(jnp.ones((num_electron_up, num_electron_up), dtype=dtype), k=-1) - K_dn = jnp.tril(jnp.ones((num_electron_dn, num_electron_dn), dtype=dtype), k=-1) + K_up = jnp.tril(jnp.ones((num_electron_up, num_electron_up), dtype=dtype_jnp), k=-1) + K_dn = jnp.tril(jnp.ones((num_electron_dn, num_electron_dn), dtype=dtype_jnp), k=-1) j1_matrix_up = j_matrix[:, -1] j1_matrix_dn = j_matrix[:, -1] @@ -1686,8 +1699,8 @@ def compute_Jastrow_three_body( j3_matrix_dn_dn = j_matrix[:, :-1] j3_matrix_up_dn = j_matrix[:, :-1] - e_up = jnp.ones(num_electron_up, dtype=dtype).T - e_dn = jnp.ones(num_electron_dn, dtype=dtype).T + e_up = jnp.ones(num_electron_up, dtype=dtype_jnp).T + e_dn = jnp.ones(num_electron_dn, dtype=dtype_jnp).T # print(f"aos_up.shape={aos_up.shape}") # print(f"aos_dn.shape={aos_dn.shape}") @@ -1714,8 +1727,6 @@ def _compute_Jastrow_three_body_debug( r_dn_carts: npt.NDArray[np.float64], ) -> float: """See _api method.""" - dtype = get_dtype("jastrow") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 aos_up = jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_up_carts) aos_dn = jastrow_three_body_data.compute_orb_api(jastrow_three_body_data.orb_data, r_dn_carts) @@ -1900,8 +1911,8 @@ def apply_block_update(self, block: "VariationalParameterBlock") -> "Jastrow_dat in ``Wavefunction_data.get_variational_blocks`` and add the corresponding handling here, without touching the SR/MCMC driver. """ - dtype = get_dtype("jastrow") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_jnp = get_dtype_jnp("jastrow_eval") + dtype_np = get_dtype_np("jastrow_eval") j1 = self.jastrow_one_body_data j2 = self.jastrow_two_body_data j3 = self.jastrow_three_body_data @@ -1927,15 +1938,15 @@ def apply_block_update(self, block: "VariationalParameterBlock") -> "Jastrow_dat j3 = Jastrow_three_body_data(orb_data=j3.orb_data, j_matrix=j3_new) elif block.name == "j3_basis_exp" and j3 is not None: new_exp = np.asarray(block.values, dtype=dtype_np) - new_exp = jnp.asarray(self._symmetrize_ao_basis(j3.orb_data, new_exp), dtype=dtype) + new_exp = self._symmetrize_ao_basis(j3.orb_data, new_exp) j3 = j3.with_updated_ao_exponents(new_exp) elif block.name == "j3_basis_coeff" and j3 is not None: new_coeff = np.asarray(block.values, dtype=dtype_np) - new_coeff = jnp.asarray(self._symmetrize_ao_basis(j3.orb_data, new_coeff), dtype=dtype) + new_coeff = self._symmetrize_ao_basis(j3.orb_data, new_coeff) j3 = j3.with_updated_ao_coefficients(new_coeff) elif block.name == "jastrow_nn_params" and nn3 is not None: # Update NN Jastrow parameters: block.values is the flattened parameter vector. - flat = jnp.asarray(block.values, dtype=dtype).reshape(-1) + flat = jnp.asarray(block.values, dtype=dtype_jnp).reshape(-1) params_new = nn3.unflatten_fn(flat) nn3 = nn3.replace(params=params_new) @@ -2048,9 +2059,9 @@ def compute_Jastrow_part(jastrow_data: Jastrow_data, r_up_carts: jax.Array, r_dn Returns: float: Total Jastrow value before exponentiation. """ - dtype = get_dtype("jastrow") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + # r_*_carts forwarded unchanged to the sub-Jastrow kernels (each handles + # its own zone management). Do not pre-cast. + dtype_jnp = get_dtype_jnp("jastrow_eval") J1 = 0.0 J2 = 0.0 @@ -2074,10 +2085,10 @@ def compute_Jastrow_part(jastrow_data: Jastrow_data, r_up_carts: jax.Array, r_dn if nn3.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") - R_n = jnp.asarray(nn3.structure_data.positions, dtype=dtype) - Z_n = jnp.asarray(nn3.structure_data.atomic_numbers, dtype=dtype) + R_n = nn3.structure_data._positions_cart_jnp.astype(dtype_jnp) + Z_n = jnp.asarray(nn3.structure_data.atomic_numbers, dtype=dtype_jnp) nn_params = jax.tree_util.tree_map( - lambda x: x.astype(dtype) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params + lambda x: x.astype(dtype_jnp) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params ) J3_nn = nn3.nn_def.apply({"params": nn_params}, r_up_carts, r_dn_carts, R_n, Z_n) J3 = J3 + J3_nn @@ -2091,8 +2102,8 @@ def _compute_Jastrow_part_debug( jastrow_data: Jastrow_data, r_up_carts: npt.NDArray[np.float64], r_dn_carts: npt.NDArray[np.float64] ) -> float: """See compute_Jastrow_part_jax for more details.""" - dtype = get_dtype("jastrow") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_jnp = get_dtype_jnp("jastrow_eval") + dtype_np = get_dtype_np("jastrow_eval") J1 = 0.0 J2 = 0.0 J3 = 0.0 @@ -2120,14 +2131,14 @@ def _compute_Jastrow_part_debug( # Use JAX NN for debug as well; convert inputs to jnp and back to float nn_params = jax.tree_util.tree_map( - lambda x: x.astype(dtype) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params + lambda x: x.astype(dtype_jnp) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params ) J3_nn = nn3.nn_def.apply( {"params": nn_params}, - jnp.asarray(r_up_carts, dtype=dtype), - jnp.asarray(r_dn_carts, dtype=dtype), - jnp.asarray(R_n, dtype=dtype), - jnp.asarray(Z_n, dtype=dtype), + jnp.asarray(r_up_carts, dtype=dtype_jnp), + jnp.asarray(r_dn_carts, dtype=dtype_jnp), + jnp.asarray(R_n, dtype=dtype_jnp), + jnp.asarray(Z_n, dtype=dtype_jnp), ) J3 += float(J3_nn) @@ -2169,11 +2180,13 @@ def _compute_ratio_Jastrow_part_rank1_update( grid generated by the MCMC loop, where exactly one electron is displaced per grid point by construction. """ - dtype = get_dtype("mcmc") - old_r_up_carts = jnp.asarray(old_r_up_carts, dtype=dtype) - old_r_dn_carts = jnp.asarray(old_r_dn_carts, dtype=dtype) - new_r_up_carts_arr = jnp.asarray(new_r_up_carts_arr, dtype=dtype) - new_r_dn_carts_arr = jnp.asarray(new_r_dn_carts_arr, dtype=dtype) + # Forward old/new r_up/dn_carts as-is (Principle 3a — no parameter rebind). + # Module-level forwards (compute_Jastrow_part, compute_Jastrow_one_body, + # compute_orb_api, NN_Jastrow.apply) handle their own use-site casts. + # Inline arithmetic in the local J1/J2/J3 closures below casts at the diff + # site (Principle 3b) — for r-r differences the operand is reconstructed in + # caller-supplied precision (fp64 from MCMC walker state) before downcast. + dtype_jnp = get_dtype_jnp("jastrow_ratio") num_up = old_r_up_carts.shape[0] num_dn = old_r_dn_carts.shape[0] @@ -2183,7 +2196,7 @@ def _compute_ratio_Jastrow_part_rank1_update( jastrow_xp = vmap(compute_Jastrow_part, in_axes=(None, 0, 0))(jastrow_data, new_r_up_carts_arr, new_r_dn_carts_arr) return jnp.exp(jastrow_xp - jastrow_x) - J_ratio = jnp.ones(n_grid, dtype=dtype) + J_ratio = jnp.ones(n_grid, dtype=dtype_jnp) # J1 part if jastrow_data.jastrow_one_body_data is not None: @@ -2197,8 +2210,12 @@ def compute_one_grid_J1(j1_data, new_r_up_carts, new_r_dn_carts, old_r_up_carts, idx_dn = jnp.argmax(nonzero_dn) r_dn_new = new_r_dn_carts[idx_dn] r_dn_old = old_r_dn_carts[idx_dn] - j1_new = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_new, axis=0)) - j1_old = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_old, axis=0)) + j1_new = compute_Jastrow_one_body( + j1_data, jnp.zeros((0, 3), dtype=dtype_jnp), jnp.expand_dims(r_dn_new, axis=0) + ) + j1_old = compute_Jastrow_one_body( + j1_data, jnp.zeros((0, 3), dtype=dtype_jnp), jnp.expand_dims(r_dn_old, axis=0) + ) return jnp.exp(j1_new - j1_old) elif num_dn == 0: @@ -2209,8 +2226,12 @@ def compute_one_grid_J1(j1_data, new_r_up_carts, new_r_dn_carts, old_r_up_carts, idx_up = jnp.argmax(nonzero_up) r_up_new = new_r_up_carts[idx_up] r_up_old = old_r_up_carts[idx_up] - j1_new = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3), dtype=dtype)) - j1_old = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3), dtype=dtype)) + j1_new = compute_Jastrow_one_body( + j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3), dtype=dtype_jnp) + ) + j1_old = compute_Jastrow_one_body( + j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3), dtype=dtype_jnp) + ) return jnp.exp(j1_new - j1_old) else: @@ -2230,10 +2251,10 @@ def up_case(args): r_up_new = new_r_up_carts[idx_up] r_up_old = old_r_up_carts[idx_up] j1_new = compute_Jastrow_one_body( - j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3), dtype=dtype) + j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3), dtype=dtype_jnp) ) j1_old = compute_Jastrow_one_body( - j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3), dtype=dtype) + j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3), dtype=dtype_jnp) ) return jnp.exp(j1_new - j1_old) @@ -2242,10 +2263,10 @@ def dn_case(args): r_dn_new = new_r_dn_carts[idx_dn] r_dn_old = old_r_dn_carts[idx_dn] j1_new = compute_Jastrow_one_body( - j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_new, axis=0) + j1_data, jnp.zeros((0, 3), dtype=dtype_jnp), jnp.expand_dims(r_dn_new, axis=0) ) j1_old = compute_Jastrow_one_body( - j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_old, axis=0) + j1_data, jnp.zeros((0, 3), dtype=dtype_jnp), jnp.expand_dims(r_dn_old, axis=0) ) return jnp.exp(j1_new - j1_old) @@ -2267,11 +2288,15 @@ def dn_case(args): def _two_body_jastrow_exp(param: float, r_cart_i: jnpt.ArrayLike, r_cart_j: jnpt.ArrayLike) -> float: """Exponential form of J2.""" - return 1.0 / (2.0 * param) * (1.0 - jnp.exp(-param * jnp.linalg.norm(r_cart_i - r_cart_j))) + # Reconstruct diff in caller-supplied precision then downcast (Principle 3b). + diff = (r_cart_i - r_cart_j).astype(dtype_jnp) + return 1.0 / (2.0 * param) * (1.0 - jnp.exp(-param * jnp.linalg.norm(diff))) def _two_body_jastrow_pade(param: float, r_cart_i: jnpt.ArrayLike, r_cart_j: jnpt.ArrayLike) -> float: """Pade form of J2.""" - return jnp.linalg.norm(r_cart_i - r_cart_j) / 2.0 * (1.0 + param * jnp.linalg.norm(r_cart_i - r_cart_j)) ** (-1.0) + # Reconstruct diff in caller-supplied precision then downcast (Principle 3b). + diff = (r_cart_i - r_cart_j).astype(dtype_jnp) + return jnp.linalg.norm(diff) / 2.0 * (1.0 + param * jnp.linalg.norm(diff)) ** (-1.0) # Select the functional form based on type if jastrow_data.jastrow_two_body_data is not None: @@ -2388,8 +2413,10 @@ def _j2_from_dist(dist, param): def compute_pairwise_sums(pos1, pos2): if pos1.shape[0] == 0 or pos2.shape[0] == 0: - return jnp.zeros(pos1.shape[0], dtype=dtype) - dists = _safe_norm(pos1[:, None, :] - pos2[None, :, :]) + return jnp.zeros(pos1.shape[0], dtype=dtype_jnp) + # Reconstruct diff in caller-supplied precision then downcast (Principle 3b). + diff = (pos1[:, None, :] - pos2[None, :, :]).astype(dtype_jnp) + dists = _safe_norm(diff) vals = _j2_from_dist(dists, j2_param) return jnp.sum(vals, axis=1) @@ -2412,9 +2439,14 @@ def compute_pairwise_sums(pos1, pos2): r_dn_old = jnp.take(old_r_dn_carts, idx_dn, axis=0) def _batch_pairwise_sum(points_a, points_b, param): - norm_a2 = jnp.sum(points_a * points_a, axis=1, keepdims=True) - norm_b2 = jnp.sum(points_b * points_b, axis=1, keepdims=True).T - dots = jnp.dot(points_a, points_b.T) + # Cast operands to the jastrow_ratio zone at the arithmetic use site + # (Principle 3b). Inputs may arrive in caller-supplied precision; cast + # before consuming as norm/dot operands to keep the function in-zone. + pa = points_a.astype(dtype_jnp) + pb = points_b.astype(dtype_jnp) + norm_a2 = jnp.sum(pa * pa, axis=1, keepdims=True) + norm_b2 = jnp.sum(pb * pb, axis=1, keepdims=True).T + dots = jnp.dot(pa, pb.T) d2 = jnp.maximum(norm_a2 + norm_b2 - 2.0 * dots, 0.0) safe_d2 = jnp.where(d2 > 0, d2, jnp.ones_like(d2)) d = jnp.where(d2 > 0, jnp.sqrt(safe_d2), jnp.zeros_like(d2)) @@ -2423,7 +2455,8 @@ def _batch_pairwise_sum(points_a, points_b, param): # Up-move branch contributions (all grids in batch) up_up_new_raw = _batch_pairwise_sum(r_up_new, old_r_up_carts, j2_param) - up_up_self = _j2_from_dist(jnp.linalg.norm(r_up_new - r_up_old, axis=1), j2_param) + # Reconstruct diff in caller-supplied precision then downcast (Principle 3b). + up_up_self = _j2_from_dist(jnp.linalg.norm((r_up_new - r_up_old).astype(dtype_jnp), axis=1), j2_param) up_up_new = up_up_new_raw - up_up_self up_up_old = jnp.take(J2_sum_up_up, idx_up, axis=0) @@ -2433,7 +2466,8 @@ def _batch_pairwise_sum(points_a, points_b, param): # Down-move branch contributions (all grids in batch) dn_dn_new_raw = _batch_pairwise_sum(r_dn_new, old_r_dn_carts, j2_param) - dn_dn_self = _j2_from_dist(jnp.linalg.norm(r_dn_new - r_dn_old, axis=1), j2_param) + # Reconstruct diff in caller-supplied precision then downcast (Principle 3b). + dn_dn_self = _j2_from_dist(jnp.linalg.norm((r_dn_new - r_dn_old).astype(dtype_jnp), axis=1), j2_param) dn_dn_new = dn_dn_new_raw - dn_dn_self dn_dn_old = jnp.take(J2_sum_dn_dn, idx_dn, axis=0) @@ -2448,12 +2482,12 @@ def _batch_pairwise_sum(points_a, points_b, param): # J3 part (batched AO evaluation — avoids per-config compute_orb_api inside vmap) if jastrow_data.jastrow_three_body_data is not None: j3d = jastrow_data.jastrow_three_body_data - j3_mat = j3d.j_matrix[:, :-1] # (n_ao, n_ao) shared for up-up / dn-dn / up-dn - j1_vec = j3d.j_matrix[:, -1] # (n_ao,) + j3_mat = j3d._j_matrix_jnp[:, :-1] # (n_ao, n_ao) shared for up-up / dn-dn / up-dn + j1_vec = j3d._j_matrix_jnp[:, -1] # (n_ao,) # Old AOs evaluated once - aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype) # (n_ao, N_up) - aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts), dtype=dtype) # (n_ao, N_dn) + aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype_jnp) # (n_ao, N_up) + aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts), dtype=dtype_jnp) # (n_ao, N_dn) N_batch = new_r_up_carts_arr.shape[0] @@ -2473,8 +2507,8 @@ def _batch_pairwise_sum(points_a, points_b, param): r_old_moved = jnp.where(up_moved_batch[:, None], r_old_up_moved, r_old_dn_moved) # (N, 3) # Single batched AO evaluation for all N configs (replaces N per-config calls inside vmap) - aos_new_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_new_moved), dtype=dtype) # (n_ao, N) - aos_old_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_old_moved), dtype=dtype) # (n_ao, N) + aos_new_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_new_moved), dtype=dtype_jnp) # (n_ao, N) + aos_old_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_old_moved), dtype=dtype_jnp) # (n_ao, N) aos_p_batch = aos_new_batch - aos_old_batch # (n_ao, N) # Precompute constant products (independent of config) @@ -2494,8 +2528,8 @@ def _batch_pairwise_sum(points_a, points_b, param): # UP formula ----------------------------------------------------------- V_up = jnp.dot(aos_p_batch.T, W_up) # (N, N_up) P_up = jnp.dot(U_up, aos_p_batch) # (N_up, N) - Q_up_c = (idx_for_Q[:, None] < jnp.arange(num_up)[None, :]).astype(dtype) # (N, N_up) - Q_up_r = (idx_for_Q[:, None] > jnp.arange(num_up)[None, :]).astype(dtype) # (N, N_up) + Q_up_c = (idx_for_Q[:, None] < jnp.arange(num_up)[None, :]).astype(dtype_jnp) # (N, N_up) + Q_up_r = (idx_for_Q[:, None] > jnp.arange(num_up)[None, :]).astype(dtype_jnp) # (N, N_up) term2_up = jnp.sum(V_up * Q_up_c, axis=1) # (N,) term3_up = jnp.sum(P_up.T * Q_up_r, axis=1) # (N,) term4_up = dn_cross_vec @ aos_p_batch # (N,) @@ -2504,8 +2538,8 @@ def _batch_pairwise_sum(points_a, points_b, param): # DN formula ----------------------------------------------------------- V_dn = jnp.dot(aos_p_batch.T, W_dn) # (N, N_dn) P_dn = jnp.dot(U_dn, aos_p_batch) # (N_dn, N) - Q_dn_c = (idx_for_Q[:, None] < jnp.arange(num_dn)[None, :]).astype(dtype) # (N, N_dn) - Q_dn_r = (idx_for_Q[:, None] > jnp.arange(num_dn)[None, :]).astype(dtype) # (N, N_dn) + Q_dn_c = (idx_for_Q[:, None] < jnp.arange(num_dn)[None, :]).astype(dtype_jnp) # (N, N_dn) + Q_dn_r = (idx_for_Q[:, None] > jnp.arange(num_dn)[None, :]).astype(dtype_jnp) # (N, N_dn) term2_dn = jnp.sum(V_dn * Q_dn_c, axis=1) # (N,) term3_dn = jnp.sum(P_dn.T * Q_dn_r, axis=1) # (N,) term4_dn = up_cross_vec @ aos_p_batch # (N,) @@ -2527,7 +2561,7 @@ def _batch_pairwise_sum(points_a, points_b, param): if nn.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") - R_n = jnp.asarray(nn.structure_data.positions, dtype=dtype) + R_n = nn.structure_data._positions_cart_jnp.astype(dtype_jnp) Z_n = jnp.asarray(nn.structure_data.atomic_numbers) def compute_one_grid_JNN(new_r_up_carts, new_r_dn_carts): @@ -2581,11 +2615,12 @@ def _compute_ratio_Jastrow_part_split_spin( exclusively for the block-structured non-local ECP grids produced by the MCMC loop. """ - dtype = get_dtype("mcmc") - old_r_up_carts = jnp.asarray(old_r_up_carts, dtype=dtype) - old_r_dn_carts = jnp.asarray(old_r_dn_carts, dtype=dtype) - new_r_up_shifted = jnp.asarray(new_r_up_shifted, dtype=dtype) - new_r_dn_shifted = jnp.asarray(new_r_dn_shifted, dtype=dtype) + # Forward old/new r_up/dn_carts as-is (Principle 3a — no parameter rebind). + # Module-level forwards (compute_Jastrow_one_body, compute_orb_api, + # _compute_ratio_Jastrow_part_rank1_update, NN_Jastrow.apply) handle their + # own use-site casts. Inline diffs in the local J2 _safe_norm closure cast + # operands to the jastrow_ratio zone at the use site (Principle 3b). + dtype_jnp = get_dtype_jnp("jastrow_ratio") num_up = old_r_up_carts.shape[0] num_dn = old_r_dn_carts.shape[0] @@ -2621,8 +2656,8 @@ def _compute_ratio_Jastrow_part_split_spin( r_up_old_moved = old_r_up_carts[idx_up_block] # (G_up, 3) r_dn_old_moved = old_r_dn_carts[idx_dn_block] # (G_dn, 3) - J_up = jnp.ones(g_up, dtype=dtype) - J_dn = jnp.ones(g_dn, dtype=dtype) + J_up = jnp.ones(g_up, dtype=dtype_jnp) + J_dn = jnp.ones(g_dn, dtype=dtype_jnp) # ── J1 part ────────────────────────────────────────────────────────────── if jastrow_data.jastrow_one_body_data is not None: @@ -2630,8 +2665,8 @@ def _compute_ratio_Jastrow_part_split_spin( # UP block: only the moved up electron contributes to the J1 change. def compute_J1_up_one(r_up_new: jax.Array, r_up_old: jax.Array) -> jax.Array: - j1_new = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3), dtype=dtype)) - j1_old = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3), dtype=dtype)) + j1_new = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_new, axis=0), jnp.zeros((0, 3), dtype=dtype_jnp)) + j1_old = compute_Jastrow_one_body(j1_data, jnp.expand_dims(r_up_old, axis=0), jnp.zeros((0, 3), dtype=dtype_jnp)) return jnp.exp(j1_new - j1_old) J1_up_block = vmap(compute_J1_up_one)(r_up_moved, r_up_old_moved) # (G_up,) @@ -2639,8 +2674,8 @@ def compute_J1_up_one(r_up_new: jax.Array, r_up_old: jax.Array) -> jax.Array: # DN block: only the moved dn electron contributes. def compute_J1_dn_one(r_dn_new: jax.Array, r_dn_old: jax.Array) -> jax.Array: - j1_new = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_new, axis=0)) - j1_old = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3), dtype=dtype), jnp.expand_dims(r_dn_old, axis=0)) + j1_new = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3), dtype=dtype_jnp), jnp.expand_dims(r_dn_new, axis=0)) + j1_old = compute_Jastrow_one_body(j1_data, jnp.zeros((0, 3), dtype=dtype_jnp), jnp.expand_dims(r_dn_old, axis=0)) return jnp.exp(j1_new - j1_old) J1_dn_block = vmap(compute_J1_dn_one)(r_dn_moved, r_dn_old_moved) # (G_dn,) @@ -2652,7 +2687,12 @@ def compute_J1_dn_one(r_dn_new: jax.Array, r_dn_old: jax.Array) -> jax.Array: _j2_type_split = jastrow_data.jastrow_two_body_data.jastrow_2b_type def _safe_norm(diff): - sq = jnp.sum(diff**2, axis=-1) + # Cast diff (reconstructed in caller-supplied precision by the + # caller, e.g. `pos1 - pos2`) to the jastrow_ratio zone at the use + # site (Principle 3b). New variable name `d` keeps the parameter + # `diff` itself frozen (Principle 3a). + d = diff.astype(dtype_jnp) + sq = jnp.sum(d**2, axis=-1) return jnp.where(sq > 0, jnp.sqrt(jnp.where(sq > 0, sq, jnp.ones_like(sq))), jnp.zeros_like(sq)) if _j2_type_split == "pade": @@ -2702,12 +2742,12 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: # ── J3 part ────────────────────────────────────────────────────────────── if jastrow_data.jastrow_three_body_data is not None: j3d = jastrow_data.jastrow_three_body_data - j3_mat = j3d.j_matrix[:, :-1] # (n_ao, n_ao) - j1_vec = j3d.j_matrix[:, -1] # (n_ao,) + j3_mat = j3d._j_matrix_jnp[:, :-1] # (n_ao, n_ao) + j1_vec = j3d._j_matrix_jnp[:, -1] # (n_ao,) # Old AOs evaluated once; column slices give old AO at each moved position. - aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype) # (n_ao, N_up) - aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts), dtype=dtype) # (n_ao, N_dn) + aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype_jnp) # (n_ao, N_up) + aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts), dtype=dtype_jnp) # (n_ao, N_dn) # Precompute constant products (shared between blocks). W_up = jnp.dot(j3_mat, aos_up_old) # (n_ao, N_up) @@ -2719,15 +2759,15 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: # ── UP BLOCK ───────────────────────────────────────────────────────── # New AOs at the moved up-electron positions; old AOs by column-slice. - aos_up_new_moved = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_up_moved), dtype=dtype) # (n_ao, G_up) + aos_up_new_moved = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_up_moved), dtype=dtype_jnp) # (n_ao, G_up) aos_up_old_moved = aos_up_old[:, idx_up_block] # (n_ao, G_up) aos_p_up = aos_up_new_moved - aos_up_old_moved # (n_ao, G_up) term1_up = j1_vec @ aos_p_up # (G_up,) V_up_block = jnp.dot(aos_p_up.T, W_up) # (G_up, N_up) P_up_block = jnp.dot(U_up, aos_p_up) # (N_up, G_up) - Q_up_c = (idx_up_block[:, None] < jnp.arange(num_up)[None, :]).astype(dtype) # (G_up, N_up) - Q_up_r = (idx_up_block[:, None] > jnp.arange(num_up)[None, :]).astype(dtype) # (G_up, N_up) + Q_up_c = (idx_up_block[:, None] < jnp.arange(num_up)[None, :]).astype(dtype_jnp) # (G_up, N_up) + Q_up_r = (idx_up_block[:, None] > jnp.arange(num_up)[None, :]).astype(dtype_jnp) # (G_up, N_up) term2_up = jnp.sum(V_up_block * Q_up_c, axis=1) # (G_up,) term3_up = jnp.sum(P_up_block.T * Q_up_r, axis=1) # (G_up,) term4_up = dn_cross_vec @ aos_p_up # (G_up,) @@ -2735,15 +2775,15 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: # ── DN BLOCK ───────────────────────────────────────────────────────── # New AOs at the moved dn-electron positions; old AOs by column-slice. - aos_dn_new_moved = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_dn_moved), dtype=dtype) # (n_ao, G_dn) + aos_dn_new_moved = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_dn_moved), dtype=dtype_jnp) # (n_ao, G_dn) aos_dn_old_moved = aos_dn_old[:, idx_dn_block] # (n_ao, G_dn) aos_p_dn = aos_dn_new_moved - aos_dn_old_moved # (n_ao, G_dn) term1_dn = j1_vec @ aos_p_dn # (G_dn,) V_dn_block = jnp.dot(aos_p_dn.T, W_dn) # (G_dn, N_dn) P_dn_block = jnp.dot(U_dn, aos_p_dn) # (N_dn, G_dn) - Q_dn_c = (idx_dn_block[:, None] < jnp.arange(num_dn)[None, :]).astype(dtype) # (G_dn, N_dn) - Q_dn_r = (idx_dn_block[:, None] > jnp.arange(num_dn)[None, :]).astype(dtype) # (G_dn, N_dn) + Q_dn_c = (idx_dn_block[:, None] < jnp.arange(num_dn)[None, :]).astype(dtype_jnp) # (G_dn, N_dn) + Q_dn_r = (idx_dn_block[:, None] > jnp.arange(num_dn)[None, :]).astype(dtype_jnp) # (G_dn, N_dn) term2_dn = jnp.sum(V_dn_block * Q_dn_c, axis=1) # (G_dn,) term3_dn = jnp.sum(P_dn_block.T * Q_dn_r, axis=1) # (G_dn,) term4_dn = up_cross_vec @ aos_p_dn # (G_dn,) @@ -2755,7 +2795,7 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: if nn.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") - R_n = jnp.asarray(nn.structure_data.positions, dtype=dtype) + R_n = nn.structure_data._positions_cart_jnp.astype(dtype_jnp) Z_n = jnp.asarray(nn.structure_data.atomic_numbers) def compute_one_grid_JNN_split(r_up: jax.Array, r_dn: jax.Array) -> jax.Array: @@ -2782,8 +2822,7 @@ def _compute_ratio_Jastrow_part_debug( new_r_dn_carts_arr: npt.NDArray[np.float64], ) -> npt.NDArray: """See _api method.""" - dtype = get_dtype("mcmc") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_ratio") return np.array( [ np.exp(compute_Jastrow_part(jastrow_data, new_r_up_carts, new_r_dn_carts)) @@ -2821,14 +2860,14 @@ def compute_grads_and_laplacian_Jastrow_part( Gradients for up/down electrons with shapes ``(N_up, 3)`` and ``(N_dn, 3)`` and Laplacians for up/down electrons with shapes ``(N_up,)`` and ``(N_dn,)``. """ - dtype = get_dtype("kinetic") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + r_up = jnp.asarray(r_up_carts, dtype=dtype_jnp) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype_jnp) grad_J_up = jnp.zeros_like(r_up) grad_J_dn = jnp.zeros_like(r_dn) - lap_J_up = jnp.zeros((r_up.shape[0],), dtype=dtype) - lap_J_dn = jnp.zeros((r_dn.shape[0],), dtype=dtype) + lap_J_up = jnp.zeros((r_up.shape[0],), dtype=dtype_jnp) + lap_J_dn = jnp.zeros((r_dn.shape[0],), dtype=dtype_jnp) # one-body (analytic) if jastrow_data.jastrow_one_body_data is not None: @@ -2872,14 +2911,14 @@ def compute_grads_and_laplacian_Jastrow_part( if nn3.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") - r_up_carts_jnp = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts_jnp = jnp.asarray(r_dn_carts, dtype=dtype) - R_n = jnp.asarray(nn3.structure_data.positions, dtype=dtype) - Z_n = jnp.asarray(nn3.structure_data.atomic_numbers, dtype=dtype) + r_up_carts_jnp = jnp.asarray(r_up_carts, dtype=dtype_jnp) + r_dn_carts_jnp = jnp.asarray(r_dn_carts, dtype=dtype_jnp) + R_n = nn3.structure_data._positions_cart_jnp.astype(dtype_jnp) + Z_n = jnp.asarray(nn3.structure_data.atomic_numbers, dtype=dtype_jnp) def _compute_Jastrow_nn_only(r_up, r_dn): nn_params = jax.tree_util.tree_map( - lambda x: x.astype(dtype) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params + lambda x: x.astype(dtype_jnp) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params ) return nn3.nn_def.apply({"params": nn_params}, r_up, r_dn, R_n, Z_n) @@ -2924,14 +2963,14 @@ def _compute_grads_and_laplacian_Jastrow_part_auto( Returns: the gradients(x,y,z) of J and the sum of laplacians of J at (r_up_carts, r_dn_carts). """ - dtype = get_dtype("kinetic") - r_up_carts_jnp = jnp.array(r_up_carts, dtype=dtype) - r_dn_carts_jnp = jnp.array(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + r_up_carts_jnp = jnp.array(r_up_carts, dtype=dtype_jnp) + r_dn_carts_jnp = jnp.array(r_dn_carts, dtype=dtype_jnp) grad_J_up = jnp.zeros_like(r_up_carts_jnp) grad_J_dn = jnp.zeros_like(r_dn_carts_jnp) - lap_J_up = jnp.zeros((r_up_carts_jnp.shape[0],), dtype=dtype) - lap_J_dn = jnp.zeros((r_dn_carts_jnp.shape[0],), dtype=dtype) + lap_J_up = jnp.zeros((r_up_carts_jnp.shape[0],), dtype=dtype_jnp) + lap_J_dn = jnp.zeros((r_dn_carts_jnp.shape[0],), dtype=dtype_jnp) # one-body if jastrow_data.jastrow_one_body_data is not None: @@ -2985,12 +3024,12 @@ def _compute_grads_and_laplacian_Jastrow_part_auto( if nn3.structure_data is None: raise ValueError("NN_Jastrow_data.structure_data must be set to evaluate NN J3.") - R_n = jnp.asarray(nn3.structure_data.positions, dtype=dtype) - Z_n = jnp.asarray(nn3.structure_data.atomic_numbers, dtype=dtype) + R_n = nn3.structure_data._positions_cart_jnp.astype(dtype_jnp) + Z_n = jnp.asarray(nn3.structure_data.atomic_numbers, dtype=dtype_jnp) def _compute_Jastrow_nn_only(r_up, r_dn): nn_params = jax.tree_util.tree_map( - lambda x: x.astype(dtype) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params + lambda x: x.astype(dtype_jnp) if hasattr(x, "dtype") and x.dtype.kind == "f" else x, nn3.params ) return nn3.nn_def.apply({"params": nn_params}, r_up, r_dn, R_n, Z_n) @@ -3026,8 +3065,7 @@ def _compute_grads_and_laplacian_Jastrow_part_debug( Uses central finite differences to approximate gradients and the sum of Laplacians of J at (r_up_carts, r_dn_carts). """ - dtype = get_dtype("kinetic") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_grad_lap") diff_h = 1.0e-5 r_up_carts = np.array(r_up_carts, dtype=dtype_np) @@ -3198,9 +3236,9 @@ def _compute_grads_and_laplacian_Jastrow_two_body_auto( # jastrow_two_body_data, r_up_carts, r_dn_carts # ) # ) - dtype = get_dtype("kinetic") - r_up_carts = jnp.array(r_up_carts, dtype=dtype) - r_dn_carts = jnp.array(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + r_up_carts = jnp.array(r_up_carts, dtype=dtype_jnp) + r_dn_carts = jnp.array(r_dn_carts, dtype=dtype_jnp) # compute grad grad_J2_up = grad(compute_Jastrow_two_body, argnums=1)(jastrow_two_body_data, r_up_carts, r_dn_carts) @@ -3238,20 +3276,20 @@ def compute_grads_and_laplacian_Jastrow_two_body( Gradients for up/down electrons with shapes ``(N_up, 3)`` and ``(N_dn, 3)``, Laplacians for up/down electrons with shapes ``(N_up,)`` and ``(N_dn,)``. """ - dtype = get_dtype("kinetic") - a = jastrow_two_body_data.jastrow_2b_param - eps = EPS_safe_distance + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + a = jnp.asarray(jastrow_two_body_data.jastrow_2b_param, dtype=dtype_jnp) + eps = jnp.asarray(EPS_safe_distance, dtype=dtype_jnp) - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + r_up = jnp.asarray(r_up_carts, dtype=dtype_jnp) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype_jnp) num_up = r_up.shape[0] num_dn = r_dn.shape[0] grad_up = jnp.zeros_like(r_up) grad_dn = jnp.zeros_like(r_dn) - lap_up = jnp.zeros((num_up,), dtype=dtype) - lap_dn = jnp.zeros((num_dn,), dtype=dtype) + lap_up = jnp.zeros((num_up,), dtype=dtype_jnp) + lap_dn = jnp.zeros((num_dn,), dtype=dtype_jnp) j2b_type = jastrow_two_body_data.jastrow_2b_type @@ -3327,8 +3365,7 @@ def _compute_grads_and_laplacian_Jastrow_two_body_debug( np.ndarray, ]: """See _api method.""" - dtype = get_dtype("kinetic") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_grad_lap") diff_h = 1.0e-5 # grad up @@ -3594,20 +3631,29 @@ def _compute_grads_and_laplacian_Jastrow_three_body_auto( Returns: the gradients(x,y,z) of J(threebody) and the sum of laplacians of J(threebody) at (r_up_carts, r_dn_carts). """ - dtype = get_dtype("kinetic") - r_up_carts = jnp.asarray(r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(r_dn_carts, dtype=dtype) + # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). Cast to + # the jastrow_grad_lap zone at the use site (Principle 3b) before passing as + # the differentiation operand to grad/hessian. + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") # compute grad - grad_J3_up = grad(compute_Jastrow_three_body, argnums=1)(jastrow_three_body_data, r_up_carts, r_dn_carts) + grad_J3_up = grad(compute_Jastrow_three_body, argnums=1)( + jastrow_three_body_data, r_up_carts.astype(dtype_jnp), r_dn_carts.astype(dtype_jnp) + ) - grad_J3_dn = grad(compute_Jastrow_three_body, argnums=2)(jastrow_three_body_data, r_up_carts, r_dn_carts) + grad_J3_dn = grad(compute_Jastrow_three_body, argnums=2)( + jastrow_three_body_data, r_up_carts.astype(dtype_jnp), r_dn_carts.astype(dtype_jnp) + ) # compute laplacians - hessian_J3_up = hessian(compute_Jastrow_three_body, argnums=1)(jastrow_three_body_data, r_up_carts, r_dn_carts) + hessian_J3_up = hessian(compute_Jastrow_three_body, argnums=1)( + jastrow_three_body_data, r_up_carts.astype(dtype_jnp), r_dn_carts.astype(dtype_jnp) + ) laplacian_J3_up = jnp.einsum("ijij->i", hessian_J3_up) - hessian_J3_dn = hessian(compute_Jastrow_three_body, argnums=2)(jastrow_three_body_data, r_up_carts, r_dn_carts) + hessian_J3_dn = hessian(compute_Jastrow_three_body, argnums=2)( + jastrow_three_body_data, r_up_carts.astype(dtype_jnp), r_dn_carts.astype(dtype_jnp) + ) laplacian_J3_dn = jnp.einsum("ijij->i", hessian_J3_dn) return grad_J3_up, grad_J3_dn, laplacian_J3_up, laplacian_J3_dn @@ -3635,7 +3681,7 @@ def compute_grads_and_laplacian_Jastrow_three_body( Gradients for up/down electrons with shapes ``(N_up, 3)`` and ``(N_dn, 3)``, Laplacians for up/down electrons with shapes ``(N_up,)`` and ``(N_dn,)``. """ - dtype = get_dtype("kinetic") + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") orb_data = jastrow_three_body_data.orb_data if isinstance(orb_data, MOs_data): @@ -3649,46 +3695,46 @@ def compute_grads_and_laplacian_Jastrow_three_body( else: raise NotImplementedError - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) - - aos_up = jnp.asarray(compute_orb(orb_data, r_up), dtype=dtype) # (n_orb, n_up) - aos_dn = jnp.asarray(compute_orb(orb_data, r_dn), dtype=dtype) # (n_orb, n_dn) + # r_*_carts forwarded unchanged to ``compute_orb`` / ``compute_orb_grad`` / + # ``compute_orb_lapl``; do not pre-cast (the AO/MO kernels reconstruct + # ``r - R`` in float64 internally). + aos_up = jnp.asarray(compute_orb(orb_data, r_up_carts), dtype=dtype_jnp) # (n_orb, n_up) + aos_dn = jnp.asarray(compute_orb(orb_data, r_dn_carts), dtype=dtype_jnp) # (n_orb, n_dn) - grad_up_x, grad_up_y, grad_up_z = compute_orb_grad(orb_data, r_up) - grad_dn_x, grad_dn_y, grad_dn_z = compute_orb_grad(orb_data, r_dn) + grad_up_x, grad_up_y, grad_up_z = compute_orb_grad(orb_data, r_up_carts) + grad_dn_x, grad_dn_y, grad_dn_z = compute_orb_grad(orb_data, r_dn_carts) grad_up = jnp.stack([grad_up_x, grad_up_y, grad_up_z], axis=-1) # (n_orb, n_up, 3) grad_dn = jnp.stack([grad_dn_x, grad_dn_y, grad_dn_z], axis=-1) # (n_orb, n_dn, 3) - lap_up = jnp.asarray(compute_orb_lapl(orb_data, r_up), dtype=dtype) # (n_orb, n_up) - lap_dn = jnp.asarray(compute_orb_lapl(orb_data, r_dn), dtype=dtype) # (n_orb, n_dn) + lap_up = jnp.asarray(compute_orb_lapl(orb_data, r_up_carts), dtype=dtype_jnp) # (n_orb, n_up) + lap_dn = jnp.asarray(compute_orb_lapl(orb_data, r_dn_carts), dtype=dtype_jnp) # (n_orb, n_dn) - j1_vec = jnp.asarray(jastrow_three_body_data.j_matrix[:, -1], dtype=dtype) # (n_orb,) - j3_mat = jnp.asarray(jastrow_three_body_data.j_matrix[:, :-1], dtype=dtype) # (n_orb, n_orb) + j1_vec = jastrow_three_body_data._j_matrix_jnp[:, -1].astype(dtype_jnp) # (n_orb,) + j3_mat = jastrow_three_body_data._j_matrix_jnp[:, :-1].astype(dtype_jnp) # (n_orb, n_orb) num_up = aos_up.shape[1] num_dn = aos_dn.shape[1] # Precompute pair-accumulation masks - upper_up = jnp.triu(jnp.ones((num_up, num_up), dtype=dtype), k=1) - lower_up = jnp.tril(jnp.ones((num_up, num_up), dtype=dtype), k=-1) - upper_dn = jnp.triu(jnp.ones((num_dn, num_dn), dtype=dtype), k=1) - lower_dn = jnp.tril(jnp.ones((num_dn, num_dn), dtype=dtype), k=-1) + upper_up = jnp.triu(jnp.ones((num_up, num_up), dtype=dtype_jnp), k=1) + lower_up = jnp.tril(jnp.ones((num_up, num_up), dtype=dtype_jnp), k=-1) + upper_dn = jnp.triu(jnp.ones((num_dn, num_dn), dtype=dtype_jnp), k=1) + lower_dn = jnp.tril(jnp.ones((num_dn, num_dn), dtype=dtype_jnp), k=-1) # dJ/dA for each electron (orbital-space coefficients) g_up = ( j1_vec[:, None] + jnp.dot(j3_mat, aos_up) @ lower_up + jnp.dot(j3_mat.T, aos_up) @ upper_up - + jnp.dot(j3_mat, aos_dn) @ jnp.ones((num_dn, 1), dtype=dtype) + + jnp.dot(j3_mat, aos_dn) @ jnp.ones((num_dn, 1), dtype=dtype_jnp) ) # (n_orb, n_up) g_dn = ( j1_vec[:, None] + jnp.dot(j3_mat, aos_dn) @ lower_dn + jnp.dot(j3_mat.T, aos_dn) @ upper_dn - + jnp.dot(j3_mat.T, aos_up) @ jnp.ones((num_up, 1), dtype=dtype) + + jnp.dot(j3_mat.T, aos_up) @ jnp.ones((num_up, 1), dtype=dtype_jnp) ) # (n_orb, n_dn) grad_J3_up = jnp.einsum("on,onj->nj", g_up, grad_up) @@ -3711,8 +3757,7 @@ def _compute_grads_and_laplacian_Jastrow_three_body_debug( np.ndarray, ]: """See _api method.""" - dtype = get_dtype("kinetic") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = get_dtype_np("jastrow_grad_lap") diff_h = 1.0e-5 # grad up diff --git a/jqmc/jqmc_cli.py b/jqmc/jqmc_cli.py index f204fc9d..8c3da3c3 100644 --- a/jqmc/jqmc_cli.py +++ b/jqmc/jqmc_cli.py @@ -49,6 +49,8 @@ # jQMC from ._header_footer import _print_footer, _print_header from ._precision import configure as configure_precision +from ._precision import mode_label as precision_mode_label +from ._precision import zone_detail as precision_zone_detail from ._setting import ( GFMC_MIN_BIN_BLOCKS, GFMC_MIN_COLLECT_STEPS, @@ -257,6 +259,8 @@ def _cli(): sorted(extra_keys), ) configure_precision(precision_mode) + logger.info("Precision: %s", precision_mode_label()) + logger.debug("Precision zone detail:\n%s", precision_zone_detail()) logger.info("") # default parameters diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index dcdda96b..a7a64de2 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -57,7 +57,7 @@ from ._diff_mask import DiffMask, apply_diff_mask from ._jqmc_utility import _generate_init_electron_configurations -from ._precision import get_dtype, get_tolerance_min +from ._precision import get_tolerance_min from ._setting import ( GFMC_MIN_BIN_BLOCKS, GFMC_MIN_COLLECT_STEPS, @@ -279,9 +279,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - dtype = get_dtype("gfmc") - self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) - self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) + dtype_jnp = jnp.float64 + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype_jnp) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -319,8 +319,7 @@ def __init_attributes(self): n_atoms = self.__hamiltonian_data.structure_data.natom # gfmc zone dtype for stored numpy arrays - dtype = get_dtype("gfmc") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = np.float64 # stored weight (w_L) self.__stored_w_L = np.zeros((0, 1), dtype=dtype_np) @@ -495,9 +494,9 @@ def load_from_hdf5(cls, filepath: str, rank: int | None = None) -> "GFMC_t": obj._GFMC_t__jax_PRNG_key_list_init = jnp.array(rng["jax_PRNG_key_list_init"]) # -- Walker state -- - dtype = get_dtype("gfmc") - obj._GFMC_t__latest_r_up_carts = jnp.asarray(ws["latest_r_up_carts"], dtype=dtype) - obj._GFMC_t__latest_r_dn_carts = jnp.asarray(ws["latest_r_dn_carts"], dtype=dtype) + dtype_jnp = jnp.float64 + obj._GFMC_t__latest_r_up_carts = jnp.asarray(ws["latest_r_up_carts"], dtype=dtype_jnp) + obj._GFMC_t__latest_r_dn_carts = jnp.asarray(ws["latest_r_dn_carts"], dtype=dtype_jnp) # -- Observables -- def _load_obs(obs_arr, default): @@ -672,8 +671,8 @@ def run(self, num_mcmc_steps: int = 50, max_time: int = 86400) -> None: np.random.seed(self.__mpi_seed) # precompute geminal inverses per walker for fast kinetic updates - dtype = get_dtype("gfmc") - eps_rcond = get_eps("rcond_svd", dtype) + dtype_jnp = jnp.float64 + eps_rcond = get_eps("rcond_svd", dtype_jnp) def _compute_initial_A_inv_t(r_up_carts, r_dn_carts): geminal = compute_geminal_all_elements( @@ -704,7 +703,7 @@ def _generate_rotation_matrix_t(alpha, beta, gamma): [cos_b * sin_g, cos_a * cos_g + sin_a * sin_b * sin_g, cos_a * sin_b * sin_g - cos_g * sin_a], [-sin_b, cos_b * sin_a, cos_a * cos_b], ], - dtype=get_dtype("gfmc"), + dtype=jnp.float64, ) return R @@ -1050,10 +1049,11 @@ def _update_inv_up_t(_): Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv det_ratio = 1.0 + (v.T @ Ainv_u)[0, 0] - # Cast back to A_old_inv.dtype: geminal elements live in the fp64 - # geminal zone, so the update would otherwise promote A_new_inv to - # fp64 and break the lax.cond dtype agreement with _no_update_t. - return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=A_old_inv.dtype) + # Consumer-zone explicit cast: cast the rank-1 update to the + # local gfmc zone dtype so the result agrees with the + # _no_update_t lax.cond branch and never depends on + # A_old_inv's upstream dtype. + return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=jnp.float64) def _no_update_t(_): return A_old_inv @@ -1079,8 +1079,8 @@ def _update_inv_dn_t(_): Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv det_ratio = 1.0 + (v.T @ Ainv_u)[0, 0] - # See note in _update_inv_up_t: cast back to A_old_inv.dtype. - return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=A_old_inv.dtype) + # See note in _update_inv_up_t: consumer-zone explicit cast. + return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=jnp.float64) if num_up_electrons == 0: A_new_inv = A_old_inv @@ -1109,8 +1109,8 @@ def _update_inv_dn_t(_): logger.info("Start compilation of the GFMC projection funciton.") logger.info(" Compilation is in progress...") projection_counter_list = jnp.array([0 for _ in range(self.__num_walkers)]) - tau_left_list = jnp.array([self.__tau for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) + tau_left_list = jnp.array([self.__tau for _ in range(self.__num_walkers)], dtype=jnp.float64) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=jnp.float64) (_, _, _, _, _, _, _, _, _) = vmap(_projection_t, in_axes=(0, 0, 0, 0, 0, 0, 0, None, None, None, None))( projection_counter_list, tau_left_list, @@ -1355,7 +1355,7 @@ def _compute_local_energy_t( start_init_force = time.perf_counter() logger.info("Start compilation of force gradient functions.") logger.info(" Compilation is in progress...") - _dummy_RTs = jnp.stack([jnp.eye(3, dtype=get_dtype("gfmc"))] * self.__num_walkers) + _dummy_RTs = jnp.stack([jnp.eye(3, dtype=jnp.float64)] * self.__num_walkers) _, _, _ = _jit_vmap_grad_e_L_t( hamiltonian_for_position_grads, self.__latest_r_up_carts, @@ -1388,8 +1388,7 @@ def _compute_local_energy_t( # -- Extend stored arrays with zero-padding for new steps -- # gfmc zone dtype for stored numpy arrays - dtype_gfmc = get_dtype("gfmc") - dtype_np = np.float64 if dtype_gfmc == jnp.float64 else np.float32 + dtype_np = np.float64 # average_projection_counter is stored on all ranks self.__stored_average_projection_counter = np.concatenate( [self.__stored_average_projection_counter, np.zeros((num_mcmc_steps,), dtype=dtype_np)] @@ -1424,8 +1423,8 @@ def _compute_local_energy_t( # Always set the initial weight list to 1.0 projection_counter_list = jnp.array([0 for _ in range(self.__num_walkers)]) - tau_left_list = jnp.array([self.__tau for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) + tau_left_list = jnp.array([self.__tau for _ in range(self.__num_walkers)], dtype=jnp.float64) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=jnp.float64) start_projection = time.perf_counter() # projection loop @@ -1551,10 +1550,10 @@ def _compute_local_energy_t( _n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up _n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn _n_atoms = self.__hamiltonian_data.structure_data.natom - omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=get_dtype("gfmc")) - omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=get_dtype("gfmc")) - grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) - grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) + omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=jnp.float64) + omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=jnp.float64) + grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=jnp.float64) + grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=jnp.float64) end_observable = time.perf_counter() timer_observable += end_observable - start_observable @@ -1946,8 +1945,8 @@ def _compute_local_energy_t( self.__num_survived_walkers += num_survived_walkers self.__num_killed_walkers += num_killed_walkers self.__stored_average_projection_counter[self.__mcmc_counter + num_mcmc_done] = ave_projection_counter - self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=get_dtype("gfmc")) - self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=get_dtype("gfmc")) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=jnp.float64) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=jnp.float64) self.__latest_A_old_inv = vmap(_compute_initial_A_inv_t, in_axes=(0, 0))( self.__latest_r_up_carts, self.__latest_r_dn_carts ) @@ -2667,9 +2666,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - dtype = get_dtype("gfmc") - self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) - self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) + dtype_jnp = jnp.float64 + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype_jnp) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -3023,8 +3022,8 @@ def _compute_local_energy_t_debug( # Always set the initial weight list to 1.0 projection_counter_list = jnp.array([0 for _ in range(self.__num_walkers)]) - e_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) + e_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=jnp.float64) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=jnp.float64) logger.devel(" Projection is on going....") @@ -3067,7 +3066,7 @@ def _compute_local_energy_t_debug( # generate a random rotation matrix jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) - R = jnp.eye(3, dtype=get_dtype("gfmc")) # Rotate in the order x -> y -> z + R = jnp.eye(3, dtype=jnp.float64) # Rotate in the order x -> y -> z # compute discretized kinetic energy and mesh (with a random rotation) mesh_kinetic_part_r_up_carts, mesh_kinetic_part_r_dn_carts, elements_non_diagonal_kinetic_part = ( @@ -3331,10 +3330,10 @@ def _compute_local_energy_t_debug( # projection ends projection_counter_list = jnp.array(projection_counter_list) - e_L_list = jnp.asarray(e_L_list, dtype=get_dtype("gfmc")) - w_L_list = jnp.asarray(w_L_list, dtype=get_dtype("gfmc")) - self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts, dtype=get_dtype("gfmc")) - self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts, dtype=get_dtype("gfmc")) + e_L_list = jnp.asarray(e_L_list, dtype=jnp.float64) + w_L_list = jnp.asarray(w_L_list, dtype=jnp.float64) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts, dtype=jnp.float64) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts, dtype=jnp.float64) self.__jax_PRNG_key_list = jnp.array(jax_PRNG_key_list) logger.debug(" Projection ends.") @@ -3342,7 +3341,7 @@ def _compute_local_energy_t_debug( # atomic force related if self.__comput_position_deriv: # RT is always eye(3) in _GFMC_t_debug (no random_discretized_mesh) - RT_eye = jnp.eye(3, dtype=get_dtype("gfmc")) + RT_eye = jnp.eye(3, dtype=jnp.float64) _grad_e_L_fn = grad(_compute_local_energy_t_debug, argnums=(0, 1, 2)) _grad_e_L_results = [ @@ -3433,10 +3432,10 @@ def _compute_local_energy_t_debug( _n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up _n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn _n_atoms = self.__hamiltonian_data.structure_data.natom - omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=get_dtype("gfmc")) - omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=get_dtype("gfmc")) - grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) - grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) + omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=jnp.float64) + omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=jnp.float64) + grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=jnp.float64) + grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=jnp.float64) # jnp.array -> np.array w_L_latest = np.array(w_L_list) @@ -3631,8 +3630,8 @@ def _compute_local_energy_t_debug( self.__num_survived_walkers += num_survived_walkers self.__num_killed_walkers += num_killed_walkers self.__stored_average_projection_counter.append(ave_projection_counter) - self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=get_dtype("gfmc")) - self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=get_dtype("gfmc")) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=jnp.float64) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=jnp.float64) # count up, here is the end of the branching step. num_mcmc_done += 1 @@ -3975,9 +3974,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - dtype = get_dtype("gfmc") - self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) - self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) + dtype_jnp = jnp.float64 + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype_jnp) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -4015,8 +4014,7 @@ def __init_attributes(self): n_atoms = self.__hamiltonian_data.structure_data.natom # gfmc zone dtype for stored numpy arrays - dtype = get_dtype("gfmc") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = np.float64 # stored weight (w_L) self.__stored_w_L = np.zeros((0, 1), dtype=dtype_np) @@ -4193,9 +4191,9 @@ def load_from_hdf5(cls, filepath: str, rank: int | None = None) -> "GFMC_n": obj._GFMC_n__jax_PRNG_key_list_init = jnp.array(rng["jax_PRNG_key_list_init"]) # -- Walker state -- - dtype = get_dtype("gfmc") - obj._GFMC_n__latest_r_up_carts = jnp.asarray(ws["latest_r_up_carts"], dtype=dtype) - obj._GFMC_n__latest_r_dn_carts = jnp.asarray(ws["latest_r_dn_carts"], dtype=dtype) + dtype_jnp = jnp.float64 + obj._GFMC_n__latest_r_up_carts = jnp.asarray(ws["latest_r_up_carts"], dtype=dtype_jnp) + obj._GFMC_n__latest_r_dn_carts = jnp.asarray(ws["latest_r_dn_carts"], dtype=dtype_jnp) # -- Observables -- n_up = obj._GFMC_n__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up @@ -4376,8 +4374,8 @@ def run(self, num_mcmc_steps: int = 50, max_time: int = 86400) -> None: gfmc_total_start = time.perf_counter() # precompute geminal inverses per walker for fast updates across projections - dtype = get_dtype("gfmc") - eps_rcond = get_eps("rcond_svd", dtype) + dtype_jnp = jnp.float64 + eps_rcond = get_eps("rcond_svd", dtype_jnp) def _compute_initial_A_inv_n(r_up_carts, r_dn_carts): geminal = compute_geminal_all_elements( @@ -4407,7 +4405,7 @@ def _generate_rotation_matrix_n(alpha, beta, gamma): [cos_b * sin_g, cos_a * cos_g + sin_a * sin_b * sin_g, cos_a * sin_b * sin_g - cos_g * sin_a], [-sin_b, cos_b * sin_a, cos_a * cos_b], ], - dtype=get_dtype("gfmc"), + dtype=jnp.float64, ) return R @@ -4762,10 +4760,11 @@ def _update_inv_up_n(_): Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv det_ratio = 1.0 + (v.T @ Ainv_u)[0, 0] - # Cast back to A_old_inv.dtype: geminal elements live in the fp64 - # geminal zone, so the update would otherwise promote A_new_inv to - # fp64 and break the lax.cond dtype agreement with _no_update_n. - return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=A_old_inv.dtype) + # Consumer-zone explicit cast: cast the rank-1 update to the + # local gfmc zone dtype so the result agrees with the + # _no_update_n lax.cond branch and never depends on + # A_old_inv's upstream dtype. + return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=jnp.float64) def _no_update_n(_): return A_old_inv @@ -4791,8 +4790,8 @@ def _update_inv_dn_n(_): Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv det_ratio = 1.0 + (v.T @ Ainv_u)[0, 0] - # See note in _update_inv_up_n: cast back to A_old_inv.dtype. - return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=A_old_inv.dtype) + # See note in _update_inv_up_n: consumer-zone explicit cast. + return jnp.asarray(A_old_inv - (Ainv_u @ vT_Ainv) / det_ratio, dtype=jnp.float64) if num_up_electrons == 0: A_new_inv = A_old_inv @@ -4844,10 +4843,10 @@ def _split_body(current_key, _): init_w_L, init_r_up_carts, init_r_dn_carts, - jnp.eye(3, dtype=get_dtype("gfmc")), + jnp.eye(3, dtype=jnp.float64), init_A_old_inv, - jnp.asarray(0.0, dtype=get_dtype("gfmc")), - jnp.asarray(0.0, dtype=get_dtype("gfmc")), + jnp.asarray(0.0, dtype=jnp.float64), + jnp.asarray(0.0, dtype=jnp.float64), ), ) @@ -4888,7 +4887,7 @@ def _compute_V_elements_n( if use_fast_update: # precompute geminal inverse for fast updates (SVD-based, robust for near-singular G) - _eps_rcond = get_eps("rcond_svd", get_dtype("gfmc")) + _eps_rcond = get_eps("rcond_svd", jnp.float64) geminal = compute_geminal_all_elements( geminal_data=hamiltonian_data.wavefunction_data.geminal_data, r_up_carts=r_up_carts, @@ -5179,7 +5178,7 @@ def _compute_local_energy_n( start_init = time.perf_counter() logger.info("Start compilation of the GFMC projection funciton.") logger.info(" Compilation is in progress...") - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=jnp.float64) ( _, _, @@ -5246,8 +5245,7 @@ def _compute_local_energy_n( n_atoms = self.__hamiltonian_data.structure_data.natom # gfmc zone dtype for stored numpy arrays - dtype_gfmc = get_dtype("gfmc") - dtype_np = np.float64 if dtype_gfmc == jnp.float64 else np.float32 + dtype_np = np.float64 self.__stored_e_L = np.concatenate([self.__stored_e_L, np.zeros((num_mcmc_steps, 1), dtype=dtype_np)]) self.__stored_e_L2 = np.concatenate([self.__stored_e_L2, np.zeros((num_mcmc_steps, 1), dtype=dtype_np)]) @@ -5279,7 +5277,7 @@ def _compute_local_energy_n( ) # Always set the initial weight list to 1.0 - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=jnp.float64) start_projection = time.perf_counter() @@ -5425,10 +5423,10 @@ def _compute_local_energy_n( _n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up _n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn _n_atoms = self.__hamiltonian_data.structure_data.natom - omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=get_dtype("gfmc")) - omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=get_dtype("gfmc")) - grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) - grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) + omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=jnp.float64) + omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=jnp.float64) + grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=jnp.float64) + grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=jnp.float64) # Barrier before MPI operation start_mpi_barrier = time.perf_counter() @@ -5813,8 +5811,8 @@ def _compute_local_energy_n( # here update the walker positions!! self.__num_survived_walkers += num_survived_walkers self.__num_killed_walkers += num_killed_walkers - self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=get_dtype("gfmc")) - self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=get_dtype("gfmc")) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=jnp.float64) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=jnp.float64) self.__latest_A_old_inv = _jit_vmap_A_inv_n(self.__latest_r_up_carts, self.__latest_r_dn_carts) mpi_comm.Barrier() @@ -6599,9 +6597,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - dtype = get_dtype("gfmc") - self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) - self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) + dtype_jnp = jnp.float64 + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype_jnp) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -6742,7 +6740,7 @@ def _generate_rotation_matrix_n_debug(alpha, beta, gamma): [cos_b * sin_g, cos_a * cos_g + sin_a * sin_b * sin_g, cos_a * sin_b * sin_g - cos_g * sin_a], [-sin_b, cos_b * sin_a, cos_a * cos_b], ], - dtype=get_dtype("gfmc"), + dtype=jnp.float64, ) return R @@ -7300,7 +7298,7 @@ def _compute_local_energy_n_debug( ) # Always set the initial weight list to 1.0 - w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=get_dtype("gfmc")) + w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=jnp.float64) logger.devel(" Projection is on going....") @@ -7357,10 +7355,11 @@ def _compute_local_energy_n_debug( for i in range(self.__num_walkers) ] ) - # e_L crosses orb_eval/jastrow/geminal/coulomb/kinetic/gfmc; bound - # the agreement by the weakest zone (fp32 in mixed precision). + # e_L crosses ao_eval/jastrow_eval/det_eval/coulomb/ + # local_energy; bound the agreement by the weakest zone (fp32 in + # mixed precision). _atol_eL, _rtol_eL = get_tolerance_min( - ("orb_eval", "jastrow", "geminal", "determinant", "coulomb", "kinetic", "gfmc"), + ("ao_eval", "jastrow_eval", "det_eval", "coulomb", "local_energy"), "strict", ) if np.max(np.abs(e_L_list - e_list_debug)) > _rtol_eL: @@ -7461,10 +7460,10 @@ def _compute_local_energy_n_debug( _n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up _n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn _n_atoms = self.__hamiltonian_data.structure_data.natom - omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=get_dtype("gfmc")) - omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=get_dtype("gfmc")) - grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) - grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=get_dtype("gfmc")) + omega_up = jnp.zeros((self.__num_walkers, _n_atoms, _n_up), dtype=jnp.float64) + omega_dn = jnp.zeros((self.__num_walkers, _n_atoms, _n_dn), dtype=jnp.float64) + grad_omega_dr_up = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=jnp.float64) + grad_omega_dr_dn = jnp.zeros((self.__num_walkers, _n_atoms, 3), dtype=jnp.float64) # jnp.array -> np.array w_L_latest = np.array(w_L_list) @@ -7669,8 +7668,8 @@ def _compute_local_energy_n_debug( # here update the walker positions!! self.__num_survived_walkers += num_survived_walkers self.__num_killed_walkers += num_killed_walkers - self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=get_dtype("gfmc")) - self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=get_dtype("gfmc")) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=jnp.float64) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=jnp.float64) # update E_scf eq_steps = GFMC_ON_THE_FLY_WARMUP_STEPS @@ -7707,8 +7706,8 @@ def get_E_on_the_fly( # logger.info(f" (w_L_eq) = {(w_L_eq)}") logger.devel(" Progress: Computing G_eq and G_e_L_eq.") - w_L_eq = jnp.asarray(w_L_eq, dtype=get_dtype("gfmc")) - e_L_eq = jnp.asarray(e_L_eq, dtype=get_dtype("gfmc")) + w_L_eq = jnp.asarray(w_L_eq, dtype=jnp.float64) + e_L_eq = jnp.asarray(e_L_eq, dtype=jnp.float64) G_eq = _compute_G_L_debug(w_L_eq, num_gfmc_collect_steps) G_e_L_eq = e_L_eq * G_eq G_eq = np.array(G_eq) diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index 46b85f55..f19da2b9 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -60,11 +60,9 @@ from ._diff_mask import DiffMask, apply_diff_mask from ._jqmc_utility import _generate_init_electron_configurations -from ._precision import get_dtype from ._setting import ( MCMC_MIN_BIN_BLOCKS, MCMC_MIN_WARMUP_STEPS, - EPS_rcond_SVD, EPS_zero_division, atol_consistency, get_eps, @@ -233,9 +231,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - dtype = get_dtype("mcmc") - self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) - self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) + dtype_jnp = jnp.float64 + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype_jnp) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -265,8 +263,7 @@ def __init_attributes(self): n_atoms = self.__hamiltonian_data.structure_data.natom # mcmc zone dtype for stored numpy arrays - dtype = get_dtype("mcmc") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_np = np.float64 # stored weight (w_L) self.__stored_w_L = np.zeros((0, nw), dtype=dtype_np) @@ -500,10 +497,10 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__latest_r_dn_carts, ) - dtype = get_dtype("mcmc") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_jnp = jnp.float64 + dtype_np = np.float64 - RTs = jnp.broadcast_to(jnp.eye(3, dtype=dtype), (len(self.__jax_PRNG_key_list), 3, 3)) + RTs = jnp.broadcast_to(jnp.eye(3, dtype=dtype_jnp), (len(self.__jax_PRNG_key_list), 3, 3)) # Warm-up compilation: trigger JIT tracing on the first run() call # so that the MCMC loop does not stall on the first step. @@ -720,7 +717,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: if self.__random_discretized_mesh: RTs = _jit_vmap_generate_RTs(self.__jax_PRNG_key_list) else: - RTs = jnp.broadcast_to(jnp.eye(3, dtype=dtype), (len(self.__jax_PRNG_key_list), 3, 3)) + RTs = jnp.broadcast_to(jnp.eye(3, dtype=dtype_jnp), (len(self.__jax_PRNG_key_list), 3, 3)) # Evaluate observables each MCMC cycle start = time.perf_counter() @@ -808,10 +805,10 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: else: n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn - omega_up_step = jnp.zeros((nw, n_atoms, n_up), dtype=dtype) - omega_dn_step = jnp.zeros((nw, n_atoms, n_dn), dtype=dtype) - grad_omega_dr_up_step = jnp.zeros((nw, n_atoms, 3), dtype=dtype) - grad_omega_dr_dn_step = jnp.zeros((nw, n_atoms, 3), dtype=dtype) + omega_up_step = jnp.zeros((nw, n_atoms, n_up), dtype=dtype_jnp) + omega_dn_step = jnp.zeros((nw, n_atoms, n_dn), dtype=dtype_jnp) + grad_omega_dr_up_step = jnp.zeros((nw, n_atoms, 3), dtype=dtype_jnp) + grad_omega_dr_dn_step = jnp.zeros((nw, n_atoms, 3), dtype=dtype_jnp) # Compute per-walker force products preserving cross-correlations _grad_e_L_r_up_np = np.array(grad_e_L_r_up_step) # (nw, n_up, 3) @@ -1402,7 +1399,7 @@ def get_dln_WF( # here, the third index indicates the flattened variational parameter index. O_matrix = np.empty((self.mcmc_counter, self.num_walkers, 0)) - for dln_Psi_dc, block in zip(dln_Psi_dc_list, matched_blocks): + for dln_Psi_dc, _ in zip(dln_Psi_dc_list, matched_blocks): logger.devel(f"dln_Psi_dc.shape={dln_Psi_dc.shape}.") if dln_Psi_dc.ndim == 2: # scalar variational param. dln_Psi_dc_reshaped = dln_Psi_dc.reshape(dln_Psi_dc.shape[0], dln_Psi_dc.shape[1], 1) @@ -2211,12 +2208,11 @@ def solve_linear_method( # ================================================================== # ---- Step 1: Remove parameters with near-zero diag(S) ---- - dtype_opt = get_dtype("optimization") - dtype_opt_np = np.float64 if dtype_opt == jnp.float64 else np.float32 + dtype_mcmc_np = np.float64 diag_S = np.diag(S_matrix) max_diag_S = np.max(np.abs(diag_S)) # parcut2 ~ machine_precision^2, effectively only removes exact zeros - parcut2 = np.finfo(dtype_opt_np).eps ** 2 + parcut2 = np.finfo(dtype_mcmc_np).eps ** 2 alive = np.abs(diag_S) > parcut2 * max_diag_S n_removed_step1 = p - int(np.count_nonzero(alive)) if n_removed_step1 > 0: @@ -2224,11 +2220,11 @@ def solve_linear_method( if not np.any(alive): logger.warning(" LM dgelscut: all parameters removed in Step 1; returning zero update.") - return np.zeros(p, dtype=dtype_opt_np), H_0 + return np.zeros(p, dtype=dtype_mcmc_np), H_0 # ---- Step 2: Build correlation matrix for alive parameters ---- alive_idx = np.where(alive)[0] - D_inv_sqrt = np.zeros(p, dtype=dtype_opt_np) + D_inv_sqrt = np.zeros(p, dtype=dtype_mcmc_np) D_inv_sqrt[alive_idx] = 1.0 / np.sqrt(np.abs(diag_S[alive_idx])) # ---- Step 3: Iteratively remove parameters until well-conditioned ---- @@ -2237,7 +2233,7 @@ def solve_linear_method( n_alive = len(idx) if n_alive == 0: logger.warning(" LM dgelscut: all parameters removed; returning zero update.") - return np.zeros(p, dtype=dtype_opt_np), H_0 + return np.zeros(p, dtype=dtype_mcmc_np), H_0 # Build correlation matrix for current alive set D_sub = D_inv_sqrt[idx] # (n_alive,) @@ -2297,7 +2293,7 @@ def solve_linear_method( if p_prime == 0: logger.warning(" LM: no positive S eigenvalues after dgelscut; returning zero update.") - return np.zeros(p, dtype=dtype_opt_np), H_0 + return np.zeros(p, dtype=dtype_mcmc_np), H_0 # P = U Λ^{-1/2} (S-orthonormal basis) inv_sqrt_Lambda = 1.0 / np.sqrt(Lambda) @@ -2309,8 +2305,8 @@ def solve_linear_method( # ---- Build extended matrices (p'+1) x (p'+1) ---- dim = p_prime + 1 - H_bar = np.zeros((dim, dim), dtype=dtype_opt_np) - S_bar = np.eye(dim, dtype=dtype_opt_np) # identity (S-orthonormal basis) + H_bar = np.zeros((dim, dim), dtype=dtype_mcmc_np) + # S_bar = np.eye(dim, dtype=dtype_mcmc_np) # identity (S-orthonormal basis) H_bar[0, 0] = H_0 H_bar[0, 1:] = -0.5 * f_new @@ -2347,7 +2343,7 @@ def solve_linear_method( # ---- Back-transform: P @ c_new → alive parameter space → full space ---- c_alive = P @ c_new # (n_alive,) - c_vec = np.zeros(p, dtype=dtype_opt_np) + c_vec = np.zeros(p, dtype=dtype_mcmc_np) c_vec[idx] = c_alive logger.info( @@ -2563,11 +2559,10 @@ def _conjugate_gradient_numpy( max_iter: int, tol: float, ) -> tuple[npt.NDArray[np.float64], float, int]: - dtype_opt = get_dtype("optimization") - dtype_opt_np = np.float64 if dtype_opt == jnp.float64 else np.float32 - x = np.array(x0, dtype=dtype_opt_np, copy=True) - r = np.array(b, dtype=dtype_opt_np, copy=False) - apply_A(x) - p = np.array(r, dtype=dtype_opt_np, copy=True) + dtype_mcmc_np = np.float64 + x = np.array(x0, dtype=dtype_mcmc_np, copy=True) + r = np.array(b, dtype=dtype_mcmc_np, copy=False) - apply_A(x) + p = np.array(r, dtype=dtype_mcmc_np, copy=True) rs_old = float(np.dot(r, r)) if not np.isfinite(rs_old): @@ -2576,7 +2571,7 @@ def _conjugate_gradient_numpy( if np.sqrt(rs_old) <= tol: return x, np.sqrt(rs_old), 0 - tiny = np.finfo(dtype_opt_np).tiny + tiny = np.finfo(dtype_mcmc_np).tiny num_iter = 0 for i in range(int(max_iter)): Ap = apply_A(p) @@ -2702,8 +2697,8 @@ def _conjugate_gradient_numpy( logger.info(f"Bin blocks = {num_mcmc_bin_blocks}.") logger.info("") - dtype_opt = get_dtype("optimization") - dtype_opt_np = np.float64 if dtype_opt == jnp.float64 else np.float32 + dtype_mcmc_jnp = jnp.float64 + dtype_mcmc_np = np.float64 lambda_projectors = None num_orb_projection = None @@ -2712,13 +2707,13 @@ def _conjugate_gradient_numpy( geminal_mo_current = wavefunction_data_step.geminal_data num_orb_projection = int(geminal_mo_current.num_electron_dn) - mo_coefficients_up = np.asarray(geminal_mo_current.orb_data_up_spin.mo_coefficients, dtype=dtype_opt_np) - mo_coefficients_dn = np.asarray(geminal_mo_current.orb_data_dn_spin.mo_coefficients, dtype=dtype_opt_np) + mo_coefficients_up = np.asarray(geminal_mo_current.orb_data_up_spin.mo_coefficients, dtype=dtype_mcmc_np) + mo_coefficients_dn = np.asarray(geminal_mo_current.orb_data_dn_spin.mo_coefficients, dtype=dtype_mcmc_np) overlap_up = np.asarray( - compute_overlap_matrix(geminal_mo_current.orb_data_up_spin.aos_data), dtype=dtype_opt_np + compute_overlap_matrix(geminal_mo_current.orb_data_up_spin.aos_data), dtype=dtype_mcmc_np ) overlap_dn = np.asarray( - compute_overlap_matrix(geminal_mo_current.orb_data_dn_spin.aos_data), dtype=dtype_opt_np + compute_overlap_matrix(geminal_mo_current.orb_data_dn_spin.aos_data), dtype=dtype_mcmc_np ) overlap_up = 0.5 * (overlap_up + overlap_up.T) overlap_dn = 0.5 * (overlap_dn + overlap_dn.T) @@ -2755,7 +2750,7 @@ def _conjugate_gradient_numpy( # ------------------------------------------------------------------ # DEVEL: orthogonal complement-projector diagnostics (I - L') and (I - R') # ------------------------------------------------------------------ - _I = np.eye(left_projector.shape[0], dtype=dtype_opt_np) + _I = np.eye(left_projector.shape[0], dtype=dtype_mcmc_np) _comp_L = _I - left_projector # (I - L') — symmetric _comp_R = _I - right_projector # (I - R') — symmetric @@ -2876,14 +2871,14 @@ def _conjugate_gradient_numpy( if not (use_sr or use_lm): if blocks: flat_param_vector = np.concatenate( - [np.ravel(np.array(block.values, dtype=dtype_opt_np)) for block in blocks] + [np.ravel(np.array(block.values, dtype=dtype_mcmc_np)) for block in blocks] ) else: - flat_param_vector = np.array([], dtype=dtype_opt_np) + flat_param_vector = np.array([], dtype=dtype_mcmc_np) if optax_state is None: optax_param_size = flat_param_vector.size - optax_state = optax_tx.init(jnp.asarray(flat_param_vector, dtype=dtype_opt)) + optax_state = optax_tx.init(jnp.asarray(flat_param_vector, dtype=dtype_mcmc_jnp)) elif flat_param_vector.size != optax_param_size: raise ValueError("The number of variational parameters changed after initializing the optax optimizer.") @@ -3038,7 +3033,7 @@ def _conjugate_gradient_numpy( # compute X_w@F X_F_local = X_local @ F_local # shape (num_param, ) - X_F = np.empty(X_F_local.shape, dtype=dtype_opt_np) + X_F = np.empty(X_F_local.shape, dtype=dtype_mcmc_np) mpi_comm.Allreduce(X_F_local, X_F, op=MPI.SUM) # compute f_argmax (index in reduced space) @@ -3060,7 +3055,7 @@ def _conjugate_gradient_numpy( # make the SR matrix scale-invariant (i.e., normalize) ## compute X_w@X.T diag_S_local = np.einsum("jk,kj->j", X_local, X_local.T) - diag_S = np.empty(diag_S_local.shape, dtype=dtype_opt_np) + diag_S = np.empty(diag_S_local.shape, dtype=dtype_mcmc_np) mpi_comm.Allreduce(diag_S_local, diag_S, op=MPI.SUM) logger.info(f"max. and min. diag_S = {np.max(diag_S)}, {np.min(diag_S)}.") # ------------------------------------------------------------------ @@ -3121,7 +3116,7 @@ def _conjugate_gradient_numpy( logger.devel(f"X_X_T_local.shape = {X_X_T_local.shape}.") # compute global sum of X * X^T if mpi_rank == 0: - X_X_T = np.empty(X_X_T_local.shape, dtype=dtype_opt_np) + X_X_T = np.empty(X_X_T_local.shape, dtype=dtype_mcmc_np) else: X_X_T = None mpi_comm.Reduce(X_X_T_local, X_X_T, op=MPI.SUM, root=0) @@ -3130,7 +3125,7 @@ def _conjugate_gradient_numpy( logger.devel(f"X_F_local.shape = {X_F_local.shape}.") # compute global sum of X @ F if mpi_rank == 0: - X_F = np.empty(X_F_local.shape, dtype=dtype_opt_np) + X_F = np.empty(X_F_local.shape, dtype=dtype_mcmc_np) else: X_F = None mpi_comm.Reduce(X_F_local, X_F, op=MPI.SUM, root=0) @@ -3175,9 +3170,9 @@ def apply_S_primal_numpy(v): x0 = np.zeros_like(X_F) theta_all, final_residual, num_steps = _conjugate_gradient_numpy( - np.asarray(X_F, dtype=dtype_opt_np), + np.asarray(X_F, dtype=dtype_mcmc_np), apply_S_primal_numpy, - np.asarray(x0, dtype=dtype_opt_np), + np.asarray(x0, dtype=dtype_mcmc_np), sr_cg_max_iter, sr_cg_tol, ) @@ -3247,7 +3242,7 @@ def apply_S_primal_numpy(v): logger.devel(f"X_T_X_local.shape = {X_T_X_local.shape}.") # compute global sum of X^T * X if mpi_rank == 0: - X_T_X = np.empty(X_T_X_local.shape, dtype=dtype_opt_np) + X_T_X = np.empty(X_T_X_local.shape, dtype=dtype_mcmc_np) else: X_T_X = None mpi_comm.Reduce(X_T_X_local, X_T_X, op=MPI.SUM, root=0) @@ -3256,7 +3251,7 @@ def apply_S_primal_numpy(v): F_recvcounts = mpi_comm.gather(F_local_count, root=0) if mpi_rank == 0: F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] - F = np.empty(sum(F_recvcounts), dtype=dtype_opt_np) + F = np.empty(sum(F_recvcounts), dtype=dtype_mcmc_np) else: F_displs = None F = None @@ -3278,7 +3273,7 @@ def apply_S_primal_numpy(v): # Broadcast K to all ranks so they know how big each chunk is K = mpi_comm.bcast(K, root=0) - X_T_X_inv_F_local = np.empty(K, dtype=dtype_opt_np) + X_T_X_inv_F_local = np.empty(K, dtype=dtype_mcmc_np) mpi_comm.Scatter( [X_T_X_inv_F, MPI.DOUBLE], # send buffer (only significant on root) @@ -3287,7 +3282,7 @@ def apply_S_primal_numpy(v): ) # theta = X_w (X^T X_w + eps*I)^{-1} F theta_all_local = X_local @ X_T_X_inv_F_local - theta_all = np.empty(theta_all_local.shape, dtype=dtype_opt_np) + theta_all = np.empty(theta_all_local.shape, dtype=dtype_mcmc_np) mpi_comm.Allreduce(theta_all_local, theta_all, op=MPI.SUM) logger.devel(f"[new] theta_all (w/ the push through identity) = {theta_all}.") logger.devel( @@ -3309,7 +3304,7 @@ def apply_dual_S_numpy(v): F_local_count = F_local.shape[0] F_recvcounts = mpi_comm.allgather(F_local_count) F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] - F_total = np.empty(sum(F_recvcounts), dtype=dtype_opt_np) + F_total = np.empty(sum(F_recvcounts), dtype=dtype_mcmc_np) mpi_comm.Allgatherv( [F_local, MPI.DOUBLE], [F_total, (F_recvcounts, F_displs), MPI.DOUBLE], @@ -3321,7 +3316,7 @@ def apply_dual_S_numpy(v): x_sol, final_residual, num_steps = _conjugate_gradient_numpy( F_total, apply_dual_S_numpy, - np.asarray(x0, dtype=dtype_opt_np), + np.asarray(x0, dtype=dtype_mcmc_np), sr_cg_max_iter, sr_cg_tol, ) @@ -3412,10 +3407,10 @@ def apply_dual_S_numpy(v): # optax optimizer ############################# else: - params = jnp.asarray(flat_param_vector, dtype=dtype_opt) - grads = -jnp.asarray(f, dtype=dtype_opt) + params = jnp.asarray(flat_param_vector, dtype=dtype_mcmc_jnp) + grads = -jnp.asarray(f, dtype=dtype_mcmc_jnp) updates, optax_state = optax_tx.update(grads, optax_state, params) - theta_all = np.array(updates, dtype=dtype_opt_np) + theta_all = np.array(updates, dtype=dtype_mcmc_np) if optax_param_size is None: optax_param_size = flat_param_vector.size self.__set_optimizer_runtime( @@ -3432,11 +3427,11 @@ def apply_dual_S_numpy(v): # 1) Expand theta_all to full parameter space. # ------------------------------------------------------------------ if use_sr: - theta = np.zeros(total_num_params, dtype=dtype_opt_np) + theta = np.zeros(total_num_params, dtype=dtype_mcmc_np) theta[:] = theta_all else: # optax - theta = np.zeros(total_num_params, dtype=dtype_opt_np) + theta = np.zeros(total_num_params, dtype=dtype_mcmc_np) theta[:] = theta_all # ------------------------------------------------------------------ @@ -3519,7 +3514,7 @@ def apply_dual_S_numpy(v): theta = 0.1 * g_sr else: # Back-transform: c_vec[0] = c₀ (SR direction), c_vec[1:] = c_k (individual params) - theta = np.zeros(total_num_params, dtype=dtype_opt_np) + theta = np.zeros(total_num_params, dtype=dtype_mcmc_np) theta[:] += c_vec[0] * g_sr # SR collective variable (affects all params) if lm_subspace_dim == -1 or lm_subspace_dim >= total_num_params: theta[:] += c_vec[1:] @@ -3566,7 +3561,7 @@ def apply_dual_S_numpy(v): # ------------------------------------------------------------------ if use_sr and lambda_projectors is not None and len(lambda_projectors) == 4: _left_proj, _right_proj, _, _ = lambda_projectors - _identity_proj = np.eye(_left_proj.shape[0], dtype=dtype_opt_np) + _identity_proj = np.eye(_left_proj.shape[0], dtype=dtype_mcmc_np) _comp_L = _identity_proj - _left_proj _comp_R = _identity_proj - _right_proj for _blk, _s, _e in offsets: @@ -4224,7 +4219,7 @@ def comput_e_L_param_deriv(self) -> bool: @jit def _generate_rotation_matrix(jax_PRNG_key): """Sample a random 3×3 rotation matrix (Euler angles).""" - dtype = get_dtype("mcmc") + dtype_jnp = jnp.float64 _, subkey = jax.random.split(jax_PRNG_key) alpha, beta, gamma = jax.random.uniform(subkey, shape=(3,), minval=-2 * jnp.pi, maxval=2 * jnp.pi) cos_a, sin_a = jnp.cos(alpha), jnp.sin(alpha) @@ -4236,7 +4231,7 @@ def _generate_rotation_matrix(jax_PRNG_key): [cos_b * sin_g, cos_a * cos_g + sin_a * sin_b * sin_g, cos_a * sin_b * sin_g - cos_g * sin_a], [-sin_b, cos_b * sin_a, cos_a * cos_b], ], - dtype=dtype, + dtype=dtype_jnp, ) return R.T @@ -4244,8 +4239,8 @@ def _generate_rotation_matrix(jax_PRNG_key): @jit def _geminal_inv_single(geminal_data, I, r_up_carts, r_dn_carts): """Build G and invert via SVD-based pseudoinverse (single sample).""" - dtype = get_dtype("mcmc") - eps_rcond = get_eps("rcond_svd", dtype) + dtype_jnp = jnp.float64 + eps_rcond = get_eps("rcond_svd", dtype_jnp) G = compute_geminal_all_elements( geminal_data=geminal_data, r_up_carts=r_up_carts, @@ -4260,9 +4255,9 @@ def _geminal_inv_single(geminal_data, I, r_up_carts, r_dn_carts): @jit def _geminal_inv_batched(geminal_data, r_up_batch, r_dn_batch): """Batched geminal inverse over walkers.""" - dtype = get_dtype("mcmc") + dtype_jnp = jnp.float64 N_up = r_up_batch.shape[-2] - I = jnp.eye(N_up, dtype=dtype) + I = jnp.eye(N_up, dtype=dtype_jnp) G_b, Ginv_b, lu_b, piv_b = vmap( _geminal_inv_single, in_axes=(None, None, 0, 0), @@ -4301,11 +4296,11 @@ def _update_electron_positions( updated_r_up_cart (jnpt.ArrayLike): up electron position. dim: (N_e^up, 3) updated_r_dn_cart (jnpt.ArrayLike): down electron position. dim: (N_e^down, 3) """ - dtype = get_dtype("mcmc") + dtype_jnp = jnp.float64 accepted_moves = 0 rejected_moves = 0 - r_up_carts = jnp.asarray(init_r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(init_r_dn_carts, dtype=dtype) + r_up_carts = jnp.asarray(init_r_up_carts, dtype=dtype_jnp) + r_dn_carts = jnp.asarray(init_r_dn_carts, dtype=dtype_jnp) geminal = geminal_init geminal_inv = geminal_inv_init @@ -4363,7 +4358,7 @@ def body_fun(_, carry): random_index = jax.random.randint(subkey, shape=(), minval=0, maxval=3) # plug g into g_vector - g_vector = jnp.zeros(3, dtype=dtype) + g_vector = jnp.zeros(3, dtype=dtype_jnp) g_vector = g_vector.at[random_index].set(g) new_r_cart = old_r_cart + g_vector @@ -4405,11 +4400,11 @@ def body_fun(_, carry): new_r_dn_carts_arr=jnp.expand_dims(proposed_r_dn_carts, axis=0), )[0] - # Determinant part, fast update using the matrix determinant lemma - # Cast both lax.cond branches to geminal_inv.dtype: the geminal-diff branch - # lives in the geminal zone (fp64) while jax.nn.one_hot defaults to fp32, - # so without an explicit cast the cond branches disagree in mixed precision. - _gem_dtype = geminal_inv.dtype + # Determinant part, fast update using the matrix determinant lemma. + # Consumer-zone explicit cast: cast both lax.cond branches to the local + # mcmc zone dtype. The geminal-diff branch lives in the det_eval zone + # (fp64) while jax.nn.one_hot defaults to fp32, so without an explicit + # cast the cond branches disagree in mixed precision. v = lax.cond( is_up, lambda _: jnp.asarray( @@ -4426,11 +4421,11 @@ def body_fun(_, carry): r_dn_carts=r_dn_carts, ) )[:, None], - dtype=_gem_dtype, + dtype=dtype_jnp, ), lambda _: jnp.asarray( jax.nn.one_hot(selected_electron_index, num_up_electrons)[:, None], - dtype=_gem_dtype, + dtype=dtype_jnp, ), operand=None, ) @@ -4439,7 +4434,7 @@ def body_fun(_, carry): is_up, lambda _: jnp.asarray( jax.nn.one_hot(selected_electron_index, num_up_electrons)[:, None], # (N_up, 1) - dtype=_gem_dtype, + dtype=dtype_jnp, ), lambda _: jnp.asarray( ( @@ -4454,7 +4449,7 @@ def body_fun(_, carry): r_dn_cart=jnp.reshape(r_dn_carts[selected_electron_index], (1, 3)), ) )[:, None], # -> (N_up, 1) - dtype=_gem_dtype, + dtype=dtype_jnp, ), operand=None, ) @@ -4490,7 +4485,7 @@ def body_fun(_, carry): # compute R_ratio R_ratio = (R_AS_ratio * WF_ratio) ** 2.0 - acceptance_ratio = jnp.min(jnp.array([1.0, R_ratio * T_ratio], dtype=dtype)) + acceptance_ratio = jnp.min(jnp.array([1.0, R_ratio * T_ratio], dtype=dtype_jnp)) jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) b = jax.random.uniform(subkey, shape=(), minval=0.0, maxval=1.0) @@ -4542,11 +4537,11 @@ def _update_electron_positions_only_up_electron( geminal_init, ): """Update electron positions based on the MH method (up-spin electrons only).""" - dtype = get_dtype("mcmc") + dtype_jnp = jnp.float64 accepted_moves = 0 rejected_moves = 0 - r_up_carts = jnp.asarray(init_r_up_carts, dtype=dtype) - r_dn_carts = jnp.asarray(init_r_dn_carts, dtype=dtype) + r_up_carts = jnp.asarray(init_r_up_carts, dtype=dtype_jnp) + r_dn_carts = jnp.asarray(init_r_dn_carts, dtype=dtype_jnp) geminal_inv = geminal_inv_init geminal = geminal_init @@ -4596,7 +4591,7 @@ def body_fun(_, carry): random_index = jax.random.randint(subkey, shape=(), minval=0, maxval=3) # plug g into g_vector - g_vector = jnp.zeros(3, dtype=dtype) + g_vector = jnp.zeros(3, dtype=dtype_jnp) g_vector = g_vector.at[random_index].set(g) new_r_cart = old_r_cart + g_vector @@ -4630,6 +4625,8 @@ def body_fun(_, carry): r_up_carts=r_up_carts, r_dn_carts=r_dn_carts, ) + Jastrow_T_p = jnp.asarray(Jastrow_T_p, dtype=dtype_jnp) + Jastrow_T_o = jnp.asarray(Jastrow_T_o, dtype=dtype_jnp) # Determinant part, fast update using the matrix determinant lemma v = ( @@ -4655,11 +4652,10 @@ def body_fun(_, carry): Det_T_ratio = 1.0 + (v.T @ Ainv_u)[0, 0] # scalar # (A+uv^T)^{-1} = A^{-1} - (A^{-1} u v^T A^{-1}) / (1 + v^T A^{-1} u) - # Cast back to geminal_inv.dtype: ``v`` originates in the geminal zone (fp64) - # while geminal_inv lives in its own zone (e.g. fp32 in mixed precision), so - # the rank-1 update would otherwise promote and break the lax.cond dtype - # agreement with the rejected branch. - geminal_inv_new = jnp.asarray(geminal_inv - (Ainv_u @ vT_Ainv) / Det_T_ratio, dtype=geminal_inv.dtype) + # Consumer-zone explicit cast: cast the rank-1 update to the local mcmc + # zone dtype so the result agrees with the rejected lax.cond branch and + # never depends on geminal_inv's upstream dtype. + geminal_inv_new = jnp.asarray(geminal_inv - (Ainv_u @ vT_Ainv) / Det_T_ratio, dtype=dtype_jnp) geminal_new = geminal.at[selected_electron_index, :].add(v.squeeze(-1)) @@ -4672,12 +4668,13 @@ def body_fun(_, carry): # modified trial WFs R_AS_ratio = (R_AS_p_eps / R_AS_p) / (R_AS_o_eps / R_AS_o) + Det_T_ratio = jnp.asarray(Det_T_ratio, dtype=dtype_jnp) WF_ratio = jnp.exp(Jastrow_T_p - Jastrow_T_o) * (Det_T_ratio) # compute R_ratio R_ratio = (R_AS_ratio * WF_ratio) ** 2.0 - acceptance_ratio = jnp.min(jnp.array([1.0, R_ratio * T_ratio], dtype=dtype)) + acceptance_ratio = jnp.min(jnp.array([1.0, R_ratio * T_ratio], dtype=dtype_jnp)) jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) b = jax.random.uniform(subkey, shape=(), minval=0.0, maxval=1.0) @@ -4834,9 +4831,9 @@ def __init__( logger.debug(f" dn counts: {dn_counts}") logger.debug(f" Total counts: {up_counts + dn_counts}") - dtype = get_dtype("mcmc") - self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype) - self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype) + dtype_jnp = jnp.float64 + self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) + self.__latest_r_dn_carts = jnp.asarray(r_carts_dn, dtype=dtype_jnp) logger.debug(f" initial r_up_carts= {self.__latest_r_up_carts}") logger.debug(f" initial r_dn_carts = {self.__latest_r_dn_carts}") @@ -4921,8 +4918,8 @@ def run(self, num_mcmc_steps: int = 0) -> None: logger.info("This is a debugging class! It supposed to be very slow.") logger.info("") - dtype = get_dtype("mcmc") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 + dtype_jnp = jnp.float64 + dtype_np = np.float64 # MAIN MCMC loop from here !!! logger.info("Start MCMC") @@ -5086,7 +5083,7 @@ def run(self, num_mcmc_steps: int = 0) -> None: R_ratio = (R_AS_ratio * WF_ratio) ** 2.0 logger.devel(f"R_ratio, T_ratio = {R_ratio}, {T_ratio}") - acceptance_ratio = np.min(jnp.array([1.0, R_ratio * T_ratio], dtype=dtype)) + acceptance_ratio = np.min(jnp.array([1.0, R_ratio * T_ratio], dtype=dtype_jnp)) logger.devel(f"acceptance_ratio = {acceptance_ratio}") jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) @@ -5108,8 +5105,8 @@ def run(self, num_mcmc_steps: int = 0) -> None: # store vmapped outcomes self.__accepted_moves = self.__accepted_moves + np.sum(accepted_moves_nw) self.__rejected_moves = self.__rejected_moves + np.sum(rejected_moves_nw) - self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts, dtype=dtype) - self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts, dtype=dtype) + self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts, dtype=dtype_jnp) + self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts, dtype=dtype_jnp) self.__jax_PRNG_key_list = jnp.array(jax_PRNG_key_list) # generate rotation matrices (for non-local ECPs) @@ -5131,11 +5128,11 @@ def run(self, num_mcmc_steps: int = 0) -> None: [cos_b * sin_g, cos_a * cos_g + sin_a * sin_b * sin_g, cos_a * sin_b * sin_g - cos_g * sin_a], [-sin_b, cos_b * sin_a, cos_a * cos_b], ], - dtype=dtype, + dtype=dtype_jnp, ) RTs.append(R.T) else: - RTs.append(jnp.eye(3, dtype=dtype)) + RTs.append(jnp.eye(3, dtype=dtype_jnp)) RTs = jnp.array(RTs) # evaluate observables @@ -5233,10 +5230,10 @@ def run(self, num_mcmc_steps: int = 0) -> None: n_up = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_up n_dn = self.__hamiltonian_data.wavefunction_data.geminal_data.num_electron_dn n_atoms = self.__hamiltonian_data.structure_data.natom - omega_up = jnp.zeros((self.__num_walkers, n_atoms, n_up), dtype=dtype) - omega_dn = jnp.zeros((self.__num_walkers, n_atoms, n_dn), dtype=dtype) - grad_omega_dr_up = jnp.zeros((self.__num_walkers, n_atoms, 3), dtype=dtype) - grad_omega_dr_dn = jnp.zeros((self.__num_walkers, n_atoms, 3), dtype=dtype) + omega_up = jnp.zeros((self.__num_walkers, n_atoms, n_up), dtype=dtype_jnp) + omega_dn = jnp.zeros((self.__num_walkers, n_atoms, n_dn), dtype=dtype_jnp) + grad_omega_dr_up = jnp.zeros((self.__num_walkers, n_atoms, 3), dtype=dtype_jnp) + grad_omega_dr_dn = jnp.zeros((self.__num_walkers, n_atoms, 3), dtype=dtype_jnp) self.__stored_omega_up.append(omega_up) self.__stored_omega_dn.append(omega_dn) diff --git a/jqmc/jqmc_tool.py b/jqmc/jqmc_tool.py index 0f387deb..d4db338b 100644 --- a/jqmc/jqmc_tool.py +++ b/jqmc/jqmc_tool.py @@ -40,22 +40,18 @@ # POSSIBILITY OF SUCH DAMAGE. import inspect -import os import re -import shutil import sys from enum import Enum from logging import Formatter, StreamHandler, getLogger from typing import List import click +import jax.numpy as jnp import matplotlib.pyplot as plt import numpy as np import tomlkit import typer - -import jax.numpy as jnp - from uncertainties import ufloat from ._checkpoint import ( @@ -64,7 +60,6 @@ load_hamiltonian_from_checkpoint, load_observables_from_checkpoint, ) -from ._precision import get_dtype from ._setting import ( GFMC_MIN_BIN_BLOCKS, GFMC_MIN_COLLECT_STEPS, @@ -413,9 +408,9 @@ def _get_j3_shell_counts(z: int, j3_choice: str) -> dict[int, int] | None: # 10) Reconstruct all common dataclass fields for the new AO object new_orbital_indices = [new_idx_map[aos_data.orbital_indices[p]] for p in new_prims] - dtype_io = get_dtype("io") - new_exponents = jnp.array([float(aos_data.exponents[p]) for p in new_prims], dtype=dtype_io) - new_coefficients = jnp.array([float(aos_data.coefficients[p]) for p in new_prims], dtype=dtype_io) + dtype_io_jnp = jnp.float64 + new_exponents = jnp.array([float(aos_data.exponents[p]) for p in new_prims], dtype=dtype_io_jnp) + new_coefficients = jnp.array([float(aos_data.coefficients[p]) for p in new_prims], dtype=dtype_io_jnp) new_nucleus_index = [aos_data.nucleus_index[i] for i in selected_ao_indices] new_angular_momentums = [aos_data.angular_momentums[i] for i in selected_ao_indices] diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index aa074590..2b635550 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -1,8 +1,8 @@ """Molecular Orbital module. Precision Zones: - - ``orb_eval``: forward MO evaluation (compute_MOs). - - ``kinetic``: MO gradient and Laplacian (compute_MOs_grad, compute_MOs_laplacian). + - ``mo_eval``: forward MO evaluation (compute_MOs). + - ``mo_grad_lap``: MO gradient and Laplacian (compute_MOs_grad, compute_MOs_laplacian). See :mod:`jqmc._precision` for details. """ @@ -50,10 +50,11 @@ import numpy.typing as npt from flax import struct from jax import jit -from jax import typing as jnpt + +from ._jqmc_utility import _cart_to_spherical_matrix, _spherical_to_cart_matrix # noqa: F401 # myqmc module -from ._precision import get_dtype +from ._precision import get_dtype_jnp from .atomic_orbital import ( AOs_cart_data, AOs_sphe_data, @@ -66,7 +67,6 @@ compute_AOs_grad, compute_AOs_laplacian, ) -from ._jqmc_utility import _cart_to_spherical_matrix, _spherical_to_cart_matrix # noqa: F401 # set logger logger = getLogger("jqmc").getChild(__name__) @@ -86,8 +86,8 @@ class MOs_data: num_mo (int): Number of molecular orbitals. aos_data (AOs_sphe_data | AOs_cart_data): AO definition supplying centers, exponents/coefficients, angular data, and contraction mapping. - mo_coefficients (npt.NDArray | jax.Array): Coefficient matrix of shape ``(num_mo, num_ao)``. Rows - correspond to MOs; columns correspond to contracted AOs. + mo_coefficients (npt.NDArray[np.float64]): Coefficient matrix of shape ``(num_mo, num_ao)``. Rows + correspond to MOs; columns correspond to contracted AOs. dtype: float64. Examples: Minimal runnable setup (2 AOs -> 1 MO):: @@ -127,8 +127,10 @@ class MOs_data: num_mo: int = struct.field(pytree_node=False, default=0) #: AO definition supplying centers, exponents/coefficients, angular data, and contraction mapping. aos_data: AOs_sphe_data | AOs_cart_data = struct.field(pytree_node=True, default_factory=lambda: AOs_sphe_data()) - #: MO coefficient matrix, shape ``(num_mo, num_ao)``. - mo_coefficients: npt.NDArray | jnpt.ArrayLike = struct.field(pytree_node=True, default_factory=lambda: np.array([])) + #: MO coefficient matrix, shape ``(num_mo, num_ao)``. dtype: float64. + mo_coefficients: npt.NDArray[np.float64] = struct.field( + pytree_node=True, default_factory=lambda: np.array([], dtype=np.float64) + ) def sanity_check(self) -> None: """Validate internal consistency. @@ -172,6 +174,13 @@ def _num_orb(self) -> int: """Return the number of orbitals.""" return self.num_mo + @property + def _mo_coefficients_jnp(self) -> jax.Array: + """Return MO coefficients as a jax.Array (jnp view of the underlying numpy storage).""" + # Lift-only fp64 basis-data storage accessor (see _precision.py exemption); + # consumer casts to its own zone at use site. + return jnp.asarray(self.mo_coefficients, dtype=jnp.float64) + def to_cartesian(self) -> "MOs_data": """Convert spherical AOs to Cartesian AOs and transform MO coefficients. @@ -188,9 +197,9 @@ def to_cartesian(self) -> "MOs_data": return self if not isinstance(self.aos_data, AOs_sphe_data): raise ValueError("Cartesian conversion is only available from spherical AOs.") - dtype = get_dtype("orb_eval") + dtype_np = np.dtype(get_dtype_jnp("mo_eval")) aos_cart, transform_matrix = _aos_sphe_to_cart(self.aos_data) - cart_coeffs = np.asarray(self.mo_coefficients, dtype=dtype) @ transform_matrix + cart_coeffs = np.asarray(self.mo_coefficients, dtype=dtype_np) @ transform_matrix cart_coeffs = cart_coeffs.astype(np.asarray(self.mo_coefficients).dtype, copy=False) return MOs_data(num_mo=self.num_mo, aos_data=aos_cart, mo_coefficients=cart_coeffs) @@ -212,9 +221,9 @@ def to_spherical(self) -> "MOs_data": return self if not isinstance(self.aos_data, AOs_cart_data): raise ValueError("Spherical conversion is only available from Cartesian AOs.") - dtype = get_dtype("orb_eval") + dtype_np = np.dtype(get_dtype_jnp("mo_eval")) aos_sphe, transform_pinv = _aos_cart_to_sphe(self.aos_data) - sph_coeffs = np.asarray(self.mo_coefficients, dtype=dtype) @ transform_pinv + sph_coeffs = np.asarray(self.mo_coefficients, dtype=dtype_np) @ transform_pinv sph_coeffs = sph_coeffs.astype(np.asarray(self.mo_coefficients).dtype, copy=False) return MOs_data(num_mo=self.num_mo, aos_data=aos_sphe, mo_coefficients=sph_coeffs) @@ -237,9 +246,9 @@ def compute_MOs(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: # ``determinant`` precision (fp64 by default). This avoids amplifying fp32 # round-off through downstream determinant / kinetic / energy paths while # preserving the speed of the AO kernels (see bug/fp32 diagnostics). - out_dtype = get_dtype("determinant") + out_dtype = get_dtype_jnp("mo_eval") aos = compute_AOs(aos_data=mos_data.aos_data, r_carts=r_carts).astype(out_dtype) - mo_coefficients = mos_data.mo_coefficients.astype(out_dtype) + mo_coefficients = mos_data._mo_coefficients_jnp.astype(out_dtype) answer = jnp.dot(mo_coefficients, aos) return answer @@ -258,7 +267,7 @@ def _compute_MOs_debug(mos_data: MOs_data, r_carts: npt.NDArray[np.float64]) -> def _compute_MOs_laplacian_autodiff(mos_data: MOs_data, r_carts: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]: """See _api method.""" mo_matrix_laplacian = jnp.dot( - mos_data.mo_coefficients, + mos_data._mo_coefficients_jnp, _compute_AOs_laplacian_autodiff(mos_data.aos_data, r_carts), ) @@ -276,10 +285,13 @@ def compute_MOs_laplacian(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: Returns: jax.Array: Laplacians of each MO, shape ``(num_mo, N_e)``. """ - dtype = get_dtype("kinetic") - mo_coefficients = mos_data.mo_coefficients.astype(dtype) + dtype_jnp = get_dtype_jnp("mo_grad_lap") + mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) ao_lap = compute_AOs_laplacian(mos_data.aos_data, r_carts) - return jnp.dot(mo_coefficients, ao_lap) + # ao_lap lives in the ao_grad_lap zone; cast to mo_grad_lap at the use site + # (Principle 3b — cast operands to this function's own zone immediately + # before consuming them as arithmetic operands). + return jnp.dot(mo_coefficients, ao_lap.astype(dtype_jnp)) def _compute_MOs_laplacian_debug(mos_data: MOs_data, r_carts: npt.NDArray[np.float64]): @@ -340,12 +352,15 @@ def compute_MOs_grad( tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64]]: Gradients per component ``(grad_x, grad_y, grad_z)``, each of shape ``(num_mo, N_e)``. """ - dtype = get_dtype("kinetic") - mo_coefficients = mos_data.mo_coefficients.astype(dtype) + dtype_jnp = get_dtype_jnp("mo_grad_lap") + mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z = compute_AOs_grad(mos_data.aos_data, r_carts) - mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x) - mo_matrix_grad_y = jnp.dot(mo_coefficients, mo_matrix_grad_y) - mo_matrix_grad_z = jnp.dot(mo_coefficients, mo_matrix_grad_z) + # AO gradient outputs live in the ao_grad_lap zone; cast to mo_grad_lap at the + # use site (Principle 3b — cast operands to this function's own zone immediately + # before consuming them as arithmetic operands). + mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x.astype(dtype_jnp)) + mo_matrix_grad_y = jnp.dot(mo_coefficients, mo_matrix_grad_y.astype(dtype_jnp)) + mo_matrix_grad_z = jnp.dot(mo_coefficients, mo_matrix_grad_z.astype(dtype_jnp)) return mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z @@ -359,12 +374,14 @@ def _compute_MOs_grad_autodiff( npt.NDArray[np.float64], ]: """This method is for computing the gradients (x,y,z) of the given molecular orbital at r_carts.""" - dtype = get_dtype("kinetic") - mo_coefficients = mos_data.mo_coefficients.astype(dtype) + dtype_jnp = get_dtype_jnp("mo_grad_lap") + mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z = _compute_AOs_grad_autodiff(mos_data.aos_data, r_carts) - mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x) - mo_matrix_grad_y = jnp.dot(mo_coefficients, mo_matrix_grad_y) - mo_matrix_grad_z = jnp.dot(mo_coefficients, mo_matrix_grad_z) + # AO gradient outputs live in the ao_grad_lap zone; cast to mo_grad_lap at the + # use site (Principle 3b). + mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x.astype(dtype_jnp)) + mo_matrix_grad_y = jnp.dot(mo_coefficients, mo_matrix_grad_y.astype(dtype_jnp)) + mo_matrix_grad_z = jnp.dot(mo_coefficients, mo_matrix_grad_z.astype(dtype_jnp)) return mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z diff --git a/jqmc/structure.py b/jqmc/structure.py index 0dad1c8b..49eabb15 100755 --- a/jqmc/structure.py +++ b/jqmc/structure.py @@ -50,11 +50,8 @@ from flax import struct from jax import jit from jax import numpy as jnp -from jax import typing as jnpt from numpy import linalg as LA -from ._precision import get_dtype - # set logger logger = getLogger("jqmc").getChild(__name__) @@ -73,7 +70,7 @@ class Structure_data: element metadata used by AOs/MOs, Coulomb, and Hamiltonian builders. Attributes: - positions (npt.NDArray | jax.Array): Atomic Cartesian coordinates with shape ``(N, 3)`` in Bohr. + positions (npt.NDArray[np.float64]): Atomic Cartesian coordinates with shape ``(N, 3)`` in Bohr. dtype: float64. pbc_flag (bool): Whether periodic boundary conditions are active. If ``True``, lattice vectors ``vec_a|b|c`` must be provided; otherwise they must be empty. vec_a (list[float] | tuple[float]): Lattice vector **a** (Bohr) when ``pbc_flag=True``. @@ -99,8 +96,8 @@ class Structure_data: structure.sanity_check() """ - #: Atomic Cartesian coordinates with shape ``(N, 3)`` in Bohr. - positions: npt.NDArray | jnpt.ArrayLike = struct.field(pytree_node=True, default_factory=lambda: np.array([])) + #: Atomic Cartesian coordinates with shape ``(N, 3)`` in Bohr. dtype: float64. + positions: npt.NDArray[np.float64] = struct.field(pytree_node=True, default_factory=lambda: np.array([], dtype=np.float64)) #: Whether periodic boundary conditions are active. pbc_flag: bool = struct.field(pytree_node=False, default=False) #: Lattice vector **a** in Bohr (requires ``pbc_flag=True``). @@ -189,8 +186,8 @@ def _logger_info(self) -> None: @property def cell(self) -> npt.NDArray: """Lattice vectors as a ``(3, 3)`` matrix in Bohr (``[a, b, c]``).""" - dtype = get_dtype("io") - cell = np.array([self.vec_a, self.vec_b, self.vec_c], dtype=dtype) + dtype_np = np.float64 + cell = np.array([self.vec_a, self.vec_b, self.vec_c], dtype=dtype_np) return cell @property @@ -206,10 +203,10 @@ def recip_cell(self) -> npt.NDArray: and asserts the orthonormality condition :math:`T_i \cdot G_j = 2\pi\,\delta_{ij}`. """ - dtype = get_dtype("io") - va = np.asarray(self.vec_a, dtype=dtype) - vb = np.asarray(self.vec_b, dtype=dtype) - vc = np.asarray(self.vec_c, dtype=dtype) + dtype_np = np.float64 + va = np.asarray(self.vec_a, dtype=dtype_np) + vb = np.asarray(self.vec_b, dtype=dtype_np) + vc = np.asarray(self.vec_c, dtype=dtype_np) recip_a = 2 * np.pi * (np.cross(vb, vc)) / (np.dot(va, np.cross(vb, vc))) recip_b = 2 * np.pi * (np.cross(vc, va)) / (np.dot(vb, np.cross(vc, va))) recip_c = 2 * np.pi * (np.cross(va, vb)) / (np.dot(vc, np.cross(va, vb))) @@ -225,7 +222,7 @@ def recip_cell(self) -> npt.NDArray: else: np.testing.assert_almost_equal(np.dot(lattice_vec, recip_vec), 0.0, decimal=15) - recip_cell = np.array([recip_a, recip_b, recip_c], dtype=dtype) + recip_cell = np.array([recip_a, recip_b, recip_c], dtype=dtype_np) return recip_cell @property @@ -321,23 +318,23 @@ def norm_vec_c(self) -> float: @property def _positions_cart_np(self) -> npt.NDArray: """Atomic positions as ``numpy.ndarray`` with shape ``(N, 3)`` in Bohr.""" - dtype = get_dtype("io") - return np.array(self.positions, dtype=dtype) + dtype_np = np.float64 + return np.array(self.positions, dtype=dtype_np) @property def _positions_cart_jnp(self) -> jax.Array: """Atomic positions as ``jax.Array`` with shape ``(N, 3)`` in Bohr.""" - dtype = get_dtype("io") - return jnp.array(self.positions, dtype=dtype) + dtype_jnp = jnp.float64 + return jnp.array(self.positions, dtype=dtype_jnp) @property def _positions_frac_np(self) -> npt.NDArray: """Fractional (crystal) coordinates as ``numpy.ndarray`` with shape ``(N, 3)``.""" - dtype = get_dtype("io") - h = np.array([self.vec_a, self.vec_b, self.vec_c], dtype=dtype) + dtype_np = np.float64 + h = np.array([self.vec_a, self.vec_b, self.vec_c], dtype=dtype_np) positions_frac = np.array( - [np.dot(np.asarray(pos, dtype=dtype), np.linalg.inv(h)) for pos in self._positions_cart_np], - dtype=dtype, + [np.dot(np.asarray(pos, dtype=dtype_np), np.linalg.inv(h)) for pos in self._positions_cart_np], + dtype=dtype_np, ) return positions_frac @@ -394,9 +391,9 @@ def _find_nearest_index_jnp(structure: Structure_data, r_cart: list[float]) -> i def _find_nearest_nucleus_indices_np(structure_data: Structure_data, r_cart, N): """See find_nearest_index.""" - dtype = get_dtype("io") + dtype_np = np.float64 positions = structure_data._positions_cart_np - r_cart = np.array(r_cart, dtype=dtype) + r_cart = np.array(r_cart, dtype=dtype_np) diffs = positions - r_cart if structure_data.pbc_flag: cell = structure_data.cell @@ -413,12 +410,12 @@ def _find_nearest_nucleus_indices_np(structure_data: Structure_data, r_cart, N): @partial(jit, static_argnums=2) def _find_nearest_nucleus_indices_jnp(structure_data: Structure_data, r_cart, N): """See find_nearest_index.""" - dtype = get_dtype("io") + dtype_jnp = jnp.float64 positions = structure_data._positions_cart_jnp - r_cart = jnp.array(r_cart, dtype=dtype) + r_cart = jnp.array(r_cart, dtype=dtype_jnp) diffs = positions - r_cart if structure_data.pbc_flag: - cell = jnp.array(structure_data.cell, dtype=dtype) + cell = jnp.array(structure_data.cell, dtype=dtype_jnp) inv_cell = jnp.linalg.inv(cell) diffs_frac = diffs @ inv_cell diffs_frac = diffs_frac - jnp.round(diffs_frac) @@ -442,8 +439,8 @@ def _get_min_dist_rel_R_cart_np(structure_data: Structure_data, r_cart: list[flo with respect to the given r_cart in cartesian. The unit is Bohr """ - dtype = get_dtype("io") - r_cart = np.array(r_cart, dtype=dtype) + dtype_np = np.float64 + r_cart = np.array(r_cart, dtype=dtype_np) R_cart = structure_data._positions_cart_np[i_atom] diff = R_cart - r_cart if structure_data.pbc_flag: @@ -465,12 +462,12 @@ def _get_min_dist_rel_R_cart_jnp( (e.g. ``jnp.float32``) and cannot be traced as an abstract array. """ if dtype is None: - dtype = get_dtype("io") - # Compute R - r in float64 to avoid catastrophic cancellation under float32 zones, - # then downcast. - _r_f64 = jnp.asarray(r_cart, dtype=jnp.float64) - _R_f64 = jnp.asarray(structure_data._positions_cart_jnp[i_atom], dtype=jnp.float64) - diff = (_R_f64 - _r_f64).astype(dtype) + dtype = jnp.float64 + # Subtract in the passed dtype (the caller chain is responsible for keeping + # high precision up to this point — see Principle 3b in jqmc._precision) + # and cast only the *result* down to the local zone. + R_cart = structure_data._positions_cart_jnp[i_atom] + diff = (R_cart - r_cart).astype(dtype) if structure_data.pbc_flag: cell = jnp.array(structure_data.cell, dtype=dtype) inv_cell = jnp.linalg.inv(cell) diff --git a/jqmc/swct.py b/jqmc/swct.py index 82395e09..5e4cb117 100644 --- a/jqmc/swct.py +++ b/jqmc/swct.py @@ -48,10 +48,9 @@ import numpy.typing as npt from jax import jacrev, jit, vmap from jax import numpy as jnp -from jax import typing as jnpt # jQMC modules -from ._precision import get_dtype +from ._precision import get_dtype_jnp, get_dtype_np from .structure import Structure_data # set logger @@ -75,13 +74,18 @@ def evaluate_swct_omega( Returns: jax.Array: Normalized weights with shape ``(N_a, N_e)``, summing to 1 over atoms for each electron. """ - dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts, dtype=dtype) - R_carts = jnp.asarray(structure_data._positions_cart_jnp, dtype=dtype) + dtype_jnp = get_dtype_jnp("swct") + R_carts = structure_data._positions_cart_jnp # fp64 storage accessor def compute_omega(R_cart, r_cart): - kappa = 1.0 / jnp.linalg.norm(r_cart - R_cart) ** 4 - kappa_sum = jnp.sum(1.0 / jnp.linalg.norm(r_cart - R_carts, axis=1) ** 4) + # Reconstruct r - R in caller-supplied precision (fp64 from MCMC walker + # state) via JAX promotion, then downcast to the swct zone at the use + # site (Principle 3b — cast operands to this function's own zone + # immediately before consuming them as arithmetic operands). + diff_one = (r_cart - R_cart).astype(dtype_jnp) + diff_all = (r_cart - R_carts).astype(dtype_jnp) + kappa = 1.0 / jnp.linalg.norm(diff_one) ** 4 + kappa_sum = jnp.sum(1.0 / jnp.linalg.norm(diff_all, axis=1) ** 4) return kappa / kappa_sum vmap_compute_omega = vmap( @@ -100,18 +104,26 @@ def compute_omega(R_cart, r_cart): def _evaluate_swct_omega_debug( structure_data: Structure_data, r_carts: npt.NDArray[np.float64], -) -> npt.NDArray[np.float64]: - """NumPy fallback for ``evaluate_swct_omega`` used in debug paths.""" - dtype = get_dtype("kinetic") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - r_carts = np.asarray(r_carts, dtype=dtype_np) - R_carts = structure_data._positions_cart_np +) -> np.ndarray: + """NumPy fallback for ``evaluate_swct_omega`` used in debug paths. + + Return dtype matches the swct zone (selectable precision); declared as a + plain ``np.ndarray`` rather than ``npt.NDArray[np.float64]``. + """ + dtype_np = get_dtype_np("swct") + R_carts = structure_data._positions_cart_np # fp64 storage accessor omega = np.zeros((len(R_carts), len(r_carts)), dtype=dtype_np) for alpha in range(len(R_carts)): for i in range(len(r_carts)): - kappa = 1.0 / np.linalg.norm(r_carts[i] - R_carts[alpha]) ** 4 - kappa_sum = np.sum([1.0 / np.linalg.norm(r_carts[i] - R_carts[beta]) ** 4 for beta in range(len(R_carts))]) + # Reconstruct r - R in caller-supplied precision (fp64 from MCMC + # walker state), then downcast to the swct zone at the use site + # (Principle 3b). + diff_one = (r_carts[i] - R_carts[alpha]).astype(dtype_np) + kappa = 1.0 / np.linalg.norm(diff_one) ** 4 + kappa_sum = np.sum( + [1.0 / np.linalg.norm((r_carts[i] - R_carts[beta]).astype(dtype_np)) ** 4 for beta in range(len(R_carts))] + ) omega[alpha, i] = kappa / kappa_sum return omega @@ -121,7 +133,7 @@ def _evaluate_swct_omega_debug( def evaluate_swct_domega( structure_data: Structure_data, r_carts: jax.Array, -) -> npt.NDArray[np.float64]: +) -> jax.Array: r"""Evaluate :math:`\sum_i \nabla_{r_i} \omega_{\alpha i}` for each atom. Args: @@ -131,8 +143,8 @@ def evaluate_swct_domega( Returns: jax.Array: Sum of gradients per atom with shape ``(N_a, 3)``. """ - dtype = get_dtype("kinetic") - r_carts = jnp.asarray(r_carts, dtype=dtype) + # Forward r_carts as-is (Principle 3a — no parameter rebind). The inner + # `evaluate_swct_omega` performs its own use-site cast to the swct zone. domega = jnp.sum(jacrev(evaluate_swct_omega, argnums=1)(structure_data, r_carts), axis=(1, 2)) return domega @@ -141,17 +153,24 @@ def evaluate_swct_domega( def _evaluate_swct_domega_debug( structure_data: Structure_data, r_carts: npt.NDArray[np.float64], -) -> npt.NDArray[np.float64]: - """NumPy fallback for ``evaluate_swct_domega`` used in debug paths.""" - dtype = get_dtype("kinetic") - dtype_np = np.float64 if dtype == jnp.float64 else np.float32 - r_carts = np.asarray(r_carts, dtype=dtype_np) - R_carts = structure_data._positions_cart_np +) -> np.ndarray: + """NumPy fallback for ``evaluate_swct_domega`` used in debug paths. + + Return dtype matches the swct zone (selectable precision); declared as a + plain ``np.ndarray`` rather than ``npt.NDArray[np.float64]``. + """ + dtype_np = get_dtype_np("swct") + R_carts = structure_data._positions_cart_np # fp64 storage accessor domega = np.zeros((len(R_carts), 3), dtype=dtype_np) def compute_omega(R_cart, r_cart, R_carts): - kappa = 1.0 / np.linalg.norm(r_cart - R_cart) ** 4 - kappa_sum = np.sum([1.0 / np.linalg.norm(r_cart - R_carts[beta]) ** 4 for beta in range(len(R_carts))]) + # Reconstruct r - R in caller-supplied precision then downcast to the + # swct zone at the use site (Principle 3b). + diff_one = (r_cart - R_cart).astype(dtype_np) + kappa = 1.0 / np.linalg.norm(diff_one) ** 4 + kappa_sum = np.sum( + [1.0 / np.linalg.norm((r_cart - R_carts[beta]).astype(dtype_np)) ** 4 for beta in range(len(R_carts))] + ) omega = kappa / kappa_sum return omega diff --git a/jqmc/trexio_wrapper.py b/jqmc/trexio_wrapper.py index f9cf0749..fbb34069 100644 --- a/jqmc/trexio_wrapper.py +++ b/jqmc/trexio_wrapper.py @@ -46,12 +46,9 @@ import numpy as np import scipy -import jax.numpy as jnp - # import trexio import trexio -from ._precision import get_dtype from .atomic_orbital import AOs_cart_data, AOs_sphe_data from .coulomb_potential import Coulomb_potential_data from .determinant import Geminal_data @@ -98,7 +95,7 @@ def read_trexio_file( >>> structure_data.atomic_labels[:3] ['O', 'H', 'H'] """ - dtype = get_dtype("io") + dtype_np = np.float64 # read a trexio file file_r = trexio.File( @@ -216,7 +213,7 @@ def read_trexio_file( atomic_numbers=tuple(_convert_from_atomic_labels_to_atomic_numbers(labels_r)), element_symbols=tuple(labels_r), atomic_labels=tuple(labels_r), - positions=np.array(coords_r, dtype=dtype), + positions=np.array(coords_r, dtype=dtype_np), ) else: structure_data = Structure_data( @@ -227,7 +224,7 @@ def read_trexio_file( atomic_numbers=list(_convert_from_atomic_labels_to_atomic_numbers(labels_r)), element_symbols=list(labels_r), atomic_labels=list(labels_r), - positions=np.array(coords_r, dtype=dtype), + positions=np.array(coords_r, dtype=dtype_np), ) # ao spherical part check @@ -339,8 +336,8 @@ def read_trexio_file( polynominal_order_y=tuple(polynominal_order_y), polynominal_order_z=tuple(polynominal_order_z), orbital_indices=tuple(orbital_indices), - exponents=jnp.array(exponents, dtype=dtype), - coefficients=jnp.array(coefficients, dtype=dtype), + exponents=np.array(exponents, dtype=dtype_np), + coefficients=np.array(coefficients, dtype=dtype_np), ) else: aos_data = AOs_cart_data( @@ -353,8 +350,8 @@ def read_trexio_file( polynominal_order_y=list(polynominal_order_y), polynominal_order_z=list(polynominal_order_z), orbital_indices=list(orbital_indices), - exponents=jnp.array(exponents, dtype=dtype), - coefficients=jnp.array(coefficients, dtype=dtype), + exponents=np.array(exponents, dtype=dtype_np), + coefficients=np.array(coefficients, dtype=dtype_np), ) else: logger.debug("Spherical basis functions.") @@ -435,8 +432,8 @@ def read_trexio_file( angular_momentums=tuple(angular_momentums), magnetic_quantum_numbers=tuple(magnetic_quantum_numbers), orbital_indices=tuple(orbital_indices), - exponents=jnp.array(exponents, dtype=dtype), - coefficients=jnp.array(coefficients, dtype=dtype), + exponents=np.array(exponents, dtype=dtype_np), + coefficients=np.array(coefficients, dtype=dtype_np), ) else: aos_data = AOs_sphe_data( @@ -447,8 +444,8 @@ def read_trexio_file( angular_momentums=list(angular_momentums), magnetic_quantum_numbers=list(magnetic_quantum_numbers), orbital_indices=list(orbital_indices), - exponents=jnp.array(exponents, dtype=dtype), - coefficients=jnp.array(coefficients, dtype=dtype), + exponents=np.array(exponents, dtype=dtype_np), + coefficients=np.array(coefficients, dtype=dtype_np), ) # MOs_data instance @@ -472,11 +469,11 @@ def read_trexio_file( num_ele_diff = num_ele_up - num_ele_dn mo_lambda_paired = np.pad( - np.eye(num_ele_dn, dtype=dtype), ((0, mo_considered_num - num_ele_dn), (0, mo_considered_num - num_ele_dn)) + np.eye(num_ele_dn, dtype=dtype_np), ((0, mo_considered_num - num_ele_dn), (0, mo_considered_num - num_ele_dn)) ) mo_lambda_unpaired = np.pad( - np.eye(num_ele_diff, dtype=dtype), ((num_ele_dn, mo_considered_num - num_ele_dn - num_ele_diff), (0, 0)) + np.eye(num_ele_diff, dtype=dtype_np), ((num_ele_dn, mo_considered_num - num_ele_dn - num_ele_diff), (0, 0)) ) mo_lambda_matrix = np.hstack([mo_lambda_paired, mo_lambda_unpaired]) @@ -508,11 +505,11 @@ def read_trexio_file( num_ele_diff = num_ele_up - num_ele_dn mo_lambda_paired = np.pad( - np.eye(num_ele_dn, dtype=dtype), ((0, mo_considered_num - num_ele_dn), (0, mo_considered_num - num_ele_dn)) + np.eye(num_ele_dn, dtype=dtype_np), ((0, mo_considered_num - num_ele_dn), (0, mo_considered_num - num_ele_dn)) ) mo_lambda_unpaired = np.pad( - np.eye(num_ele_diff, dtype=dtype), ((num_ele_dn, mo_considered_num - num_ele_dn - num_ele_diff), (0, 0)) + np.eye(num_ele_diff, dtype=dtype_np), ((num_ele_dn, mo_considered_num - num_ele_dn - num_ele_diff), (0, 0)) ) mo_lambda_matrix = np.hstack([mo_lambda_paired, mo_lambda_unpaired]) else: diff --git a/jqmc/wavefunction.py b/jqmc/wavefunction.py index 5d4ed6f3..60e2076a 100644 --- a/jqmc/wavefunction.py +++ b/jqmc/wavefunction.py @@ -54,10 +54,10 @@ from jax import typing as jnpt from ._diff_mask import DiffMask, apply_diff_mask +from ._precision import get_dtype_jnp from .atomic_orbital import AOs_cart_data, AOs_sphe_data, ShellPrimMap from .determinant import ( Geminal_data, - _compute_ratio_determinant_part_rank1_update, _compute_ratio_determinant_part_split_spin, compute_det_geminal_all_elements, compute_grads_and_laplacian_ln_Det, @@ -72,7 +72,6 @@ compute_Jastrow_part, ) from .molecular_orbital import MOs_data -from ._precision import get_dtype # set logger logger = getLogger("jqmc").getChild(__name__) @@ -682,23 +681,29 @@ def evaluate_ln_wavefunction( Returns: Scalar log-value of the wavefunction magnitude. """ - dtype_det = get_dtype("determinant") - r_up = jnp.asarray(r_up_carts, dtype=dtype_det) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype_det) + # NOTE: do not pre-cast r_*_carts. They are forwarded unchanged to + # ``compute_Jastrow_part`` and ``compute_det_geminal_all_elements`` (which + # ultimately reach the AO kernels that reconstruct ``r - R`` in float64); + # a wrapper-level downcast would defeat that precision guard. The scalar + # arithmetic below uses ``Jastrow_part`` and ``Determinant_part`` which + # are explicitly cast to ``wf_eval``. + dtype_wf_jnp = get_dtype_jnp("wf_eval") Jastrow_part = compute_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) Determinant_part = compute_det_geminal_all_elements( geminal_data=wavefunction_data.geminal_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) - return jnp.asarray(Jastrow_part, dtype=dtype_det) + jnp.log(jnp.abs(Determinant_part)) + # Consumer-zone explicit cast: both terms cast to wf_eval before addition, + # never relying on implicit fp32 + fp64 -> fp64 promotion. + return jnp.asarray(Jastrow_part, dtype=dtype_wf_jnp) + jnp.asarray(jnp.log(jnp.abs(Determinant_part)), dtype=dtype_wf_jnp) def evaluate_ln_wavefunction_fast( @@ -736,24 +741,24 @@ def evaluate_ln_wavefunction_fast( Passing an inverse from a different configuration silently produces incorrect parameter gradients (``O_matrix`` / SR). """ - dtype_det = get_dtype("determinant") - r_up = jnp.asarray(r_up_carts, dtype=dtype_det) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype_det) + # r_*_carts forwarded unchanged (see ``evaluate_ln_wavefunction`` for rationale). + dtype_wf_jnp = get_dtype_jnp("wf_eval") Jastrow_part = compute_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) ln_det = compute_ln_det_geminal_all_elements_fast( wavefunction_data.geminal_data, - r_up, - r_dn, + r_up_carts, + r_dn_carts, geminal_inv, ) - return jnp.asarray(Jastrow_part, dtype=dtype_det) + ln_det + # Consumer-zone explicit cast: both terms cast to wf_eval before addition. + return jnp.asarray(Jastrow_part, dtype=dtype_wf_jnp) + jnp.asarray(ln_det, dtype=dtype_wf_jnp) @jit @@ -776,23 +781,23 @@ def evaluate_wavefunction( Returns: Complex or real wavefunction value. """ - dtype_det = get_dtype("determinant") - r_up = jnp.asarray(r_up_carts, dtype=dtype_det) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype_det) + # r_*_carts forwarded unchanged (see ``evaluate_ln_wavefunction`` for rationale). + dtype_wf_jnp = get_dtype_jnp("wf_eval") Jastrow_part = compute_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) Determinant_part = compute_det_geminal_all_elements( geminal_data=wavefunction_data.geminal_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) - return jnp.exp(jnp.asarray(Jastrow_part, dtype=dtype_det)) * Determinant_part + # Consumer-zone explicit cast: both factors cast to wf_eval before multiplication. + return jnp.exp(jnp.asarray(Jastrow_part, dtype=dtype_wf_jnp)) * jnp.asarray(Determinant_part, dtype=dtype_wf_jnp) def evaluate_jastrow( @@ -814,14 +819,11 @@ def evaluate_jastrow( Returns: Real Jastrow factor ``exp(J)``. """ - dtype = get_dtype("jastrow") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) - + # r_*_carts forwarded unchanged (see ``evaluate_ln_wavefunction`` for rationale). Jastrow_part = compute_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) return jnp.exp(Jastrow_part) @@ -842,14 +844,11 @@ def evaluate_determinant( Returns: Determinant value evaluated at the supplied coordinates. """ - dtype = get_dtype("determinant") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) - + # r_*_carts forwarded unchanged (see ``evaluate_ln_wavefunction`` for rationale). Determinant_part = compute_det_geminal_all_elements( geminal_data=wavefunction_data.geminal_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) return Determinant_part @@ -876,16 +875,15 @@ def compute_kinetic_energy( Returns: Kinetic energy evaluated for the supplied configuration. """ - dtype = get_dtype("kinetic") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + # r_*_carts forwarded unchanged (see ``evaluate_ln_wavefunction`` for rationale). + dtype_jnp = get_dtype_jnp("wf_kinetic") # grad_J_up, grad_J_dn, sum_laplacian_J = 0.0, 0.0, 0.0 # """ grad_J_up, grad_J_dn, lap_J_up, lap_J_dn = compute_grads_and_laplacian_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) # """ @@ -893,11 +891,22 @@ def compute_kinetic_energy( # """ grad_ln_D_up, grad_ln_D_dn, lap_ln_D_up, lap_ln_D_dn = compute_grads_and_laplacian_ln_Det( geminal_data=wavefunction_data.geminal_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) # """ + # Explicitly cast jastrow_grad_lap and det_grad_lap zone values to wf_kinetic + # zone dtype before assembling T_L; do not rely on JAX implicit promotion. + grad_J_up = jnp.asarray(grad_J_up, dtype=dtype_jnp) + grad_J_dn = jnp.asarray(grad_J_dn, dtype=dtype_jnp) + lap_J_up = jnp.asarray(lap_J_up, dtype=dtype_jnp) + lap_J_dn = jnp.asarray(lap_J_dn, dtype=dtype_jnp) + grad_ln_D_up = jnp.asarray(grad_ln_D_up, dtype=dtype_jnp) + grad_ln_D_dn = jnp.asarray(grad_ln_D_dn, dtype=dtype_jnp) + lap_ln_D_up = jnp.asarray(lap_ln_D_up, dtype=dtype_jnp) + lap_ln_D_dn = jnp.asarray(lap_ln_D_dn, dtype=dtype_jnp) + # compute kinetic energy L = ( 1.0 @@ -932,9 +941,9 @@ def _compute_kinetic_energy_auto( Returns: The kinetic energy with the given wavefunction (float | complex) """ - dtype = get_dtype("kinetic") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("wf_kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype_jnp) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype_jnp) kinetic_energy_all_elements_up, kinetic_energy_all_elements_dn = _compute_kinetic_energy_all_elements_auto( wavefunction_data=wavefunction_data, r_up_carts=r_up, r_dn_carts=r_dn @@ -1010,9 +1019,9 @@ def _compute_kinetic_energy_all_elements_auto( r_dn_carts: jax.Array, ) -> jax.Array: """See compute_kinetic_energy_api.""" - dtype = get_dtype("kinetic") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("wf_kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype_jnp) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype_jnp) # compute gradients grad_J_up = grad(compute_Jastrow_part, argnums=1)(wavefunction_data.jastrow_data, r_up, r_dn) @@ -1020,6 +1029,12 @@ def _compute_kinetic_energy_all_elements_auto( grad_ln_Det_up = grad(compute_ln_det_geminal_all_elements, argnums=1)(wavefunction_data.geminal_data, r_up, r_dn) grad_ln_Det_dn = grad(compute_ln_det_geminal_all_elements, argnums=2)(wavefunction_data.geminal_data, r_up, r_dn) + # Cast jastrow/det grad/lap zone values to wf_kinetic dtype before combining. + grad_J_up = jnp.asarray(grad_J_up, dtype=dtype_jnp) + grad_J_dn = jnp.asarray(grad_J_dn, dtype=dtype_jnp) + grad_ln_Det_up = jnp.asarray(grad_ln_Det_up, dtype=dtype_jnp) + grad_ln_Det_dn = jnp.asarray(grad_ln_Det_dn, dtype=dtype_jnp) + grad_ln_Psi_up = grad_J_up + grad_ln_Det_up grad_ln_Psi_dn = grad_J_dn + grad_ln_Det_dn @@ -1034,6 +1049,11 @@ def _compute_kinetic_energy_all_elements_auto( hessian_ln_Det_dn = hessian(compute_ln_det_geminal_all_elements, argnums=2)(wavefunction_data.geminal_data, r_up, r_dn) laplacian_ln_Det_dn = jnp.einsum("ijij->i", hessian_ln_Det_dn) + laplacian_J_up = jnp.asarray(laplacian_J_up, dtype=dtype_jnp) + laplacian_J_dn = jnp.asarray(laplacian_J_dn, dtype=dtype_jnp) + laplacian_ln_Det_up = jnp.asarray(laplacian_ln_Det_up, dtype=dtype_jnp) + laplacian_ln_Det_dn = jnp.asarray(laplacian_ln_Det_dn, dtype=dtype_jnp) + laplacian_Psi_up = laplacian_J_up + laplacian_ln_Det_up laplacian_Psi_dn = laplacian_J_dn + laplacian_ln_Det_dn @@ -1064,24 +1084,33 @@ def compute_kinetic_energy_all_elements( Tuple of two ``jax.Array`` objects containing per-electron kinetic energies for spin-up and spin-down electrons, respectively. """ - dtype = get_dtype("kinetic") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + # r_*_carts forwarded unchanged (see ``evaluate_ln_wavefunction`` for rationale). + dtype_jnp = get_dtype_jnp("wf_kinetic") # --- Jastrow contributions (per-electron Laplacians) --- grad_J_up, grad_J_dn, lap_J_up, lap_J_dn = compute_grads_and_laplacian_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) # --- Determinant contributions (per-electron Laplacians) --- grad_ln_D_up, grad_ln_D_dn, lap_ln_D_up, lap_ln_D_dn = compute_grads_and_laplacian_ln_Det( geminal_data=wavefunction_data.geminal_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) + # Cast jastrow_grad_lap / det_grad_lap zone values to wf_kinetic dtype. + grad_J_up = jnp.asarray(grad_J_up, dtype=dtype_jnp) + grad_J_dn = jnp.asarray(grad_J_dn, dtype=dtype_jnp) + lap_J_up = jnp.asarray(lap_J_up, dtype=dtype_jnp) + lap_J_dn = jnp.asarray(lap_J_dn, dtype=dtype_jnp) + grad_ln_D_up = jnp.asarray(grad_ln_D_up, dtype=dtype_jnp) + grad_ln_D_dn = jnp.asarray(grad_ln_D_dn, dtype=dtype_jnp) + lap_ln_D_up = jnp.asarray(lap_ln_D_up, dtype=dtype_jnp) + lap_ln_D_dn = jnp.asarray(lap_ln_D_dn, dtype=dtype_jnp) + # --- Assemble kinetic energy per electron --- grad_ln_Psi_up = grad_J_up + grad_ln_D_up grad_ln_Psi_dn = grad_J_dn + grad_ln_D_dn @@ -1126,23 +1155,32 @@ def compute_kinetic_energy_all_elements_fast_update( if geminal_inverse is None: raise ValueError("geminal_inverse must be provided for fast update") - dtype = get_dtype("kinetic") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + # r_*_carts forwarded unchanged (see ``evaluate_ln_wavefunction`` for rationale). + dtype_jnp = get_dtype_jnp("wf_kinetic") grad_J_up, grad_J_dn, lap_J_up, lap_J_dn = compute_grads_and_laplacian_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) grad_ln_D_up, grad_ln_D_dn, lap_ln_D_up, lap_ln_D_dn = compute_grads_and_laplacian_ln_Det_fast( geminal_data=wavefunction_data.geminal_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, geminal_inverse=geminal_inverse, ) + # Cast jastrow_grad_lap / det_grad_lap zone values to wf_kinetic dtype. + grad_J_up = jnp.asarray(grad_J_up, dtype=dtype_jnp) + grad_J_dn = jnp.asarray(grad_J_dn, dtype=dtype_jnp) + lap_J_up = jnp.asarray(lap_J_up, dtype=dtype_jnp) + lap_J_dn = jnp.asarray(lap_J_dn, dtype=dtype_jnp) + grad_ln_D_up = jnp.asarray(grad_ln_D_up, dtype=dtype_jnp) + grad_ln_D_dn = jnp.asarray(grad_ln_D_dn, dtype=dtype_jnp) + lap_ln_D_up = jnp.asarray(lap_ln_D_up, dtype=dtype_jnp) + lap_ln_D_dn = jnp.asarray(lap_ln_D_dn, dtype=dtype_jnp) + grad_ln_Psi_up = grad_J_up + grad_ln_D_up grad_ln_Psi_dn = grad_J_dn + grad_ln_D_dn @@ -1279,10 +1317,10 @@ def compute_discretized_kinetic_energy( combined coordinate arrays have shapes ``(n_grid, n_up, 3)`` and ``(n_grid, n_dn, 3)`` and ``elements_kinetic_part`` contains the kinetic prefactor-scaled ratios. """ - dtype = get_dtype("kinetic") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) - rt = jnp.asarray(RT, dtype=dtype) + dtype_jnp = get_dtype_jnp("wf_kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype_jnp) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype_jnp) + rt = jnp.asarray(RT, dtype=dtype_jnp) # Define the shifts to apply (+/- alat in each coordinate direction) shifts = alat * jnp.array( [ @@ -1359,6 +1397,14 @@ def compute_discretized_kinetic_energy( det_xp = vmap(compute_det_geminal_all_elements, in_axes=(None, 0, 0))( wavefunction_data.geminal_data, r_up_carts_combined, r_dn_carts_combined ) + # Explicitly cast both jastrow (jastrow_eval zone, possibly fp32) and det + # values to the wf_ratio zone dtype so that exp() and the wf_ratio arithmetic + # do not rely on JAX implicit fp32 x fp64 -> fp64 promotion. + dtype_wf_ratio_jnp = get_dtype_jnp("wf_ratio") + jastrow_x = jnp.asarray(jastrow_x, dtype=dtype_wf_ratio_jnp) + jastrow_xp = jnp.asarray(jastrow_xp, dtype=dtype_wf_ratio_jnp) + det_x = jnp.asarray(det_x, dtype=dtype_wf_ratio_jnp) + det_xp = jnp.asarray(det_xp, dtype=dtype_wf_ratio_jnp) wf_ratio = jnp.exp(jastrow_xp - jastrow_x) * det_xp / det_x # Compute the kinetic part elements @@ -1397,10 +1443,10 @@ def compute_discretized_kinetic_energy_fast_update( coordinate arrays of shapes ``(n_grid, n_up, 3)`` and ``(n_grid, n_dn, 3)``, and kinetic prefactor-scaled ratios ``elements_kinetic_part``. """ - dtype = get_dtype("kinetic") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) - rt = jnp.asarray(RT, dtype=dtype) + dtype_jnp = get_dtype_jnp("wf_kinetic") + r_up = jnp.asarray(r_up_carts, dtype=dtype_jnp) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype_jnp) + rt = jnp.asarray(RT, dtype=dtype_jnp) # Define the shifts to apply (+/- alat in each coordinate direction) shifts = alat * jnp.array( [ @@ -1450,21 +1496,32 @@ def compute_discretized_kinetic_energy_fast_update( r_up_carts_combined = jnp.concatenate([r_up_carts_shifted, r_up_carts_repeated_dn], axis=0) # Shape: (N_configs, N_up, 3) r_dn_carts_combined = jnp.concatenate([r_dn_carts_repeated_up, r_dn_carts_shifted], axis=0) # Shape: (N_configs, N_dn, 3) - # Evaluate the ratios of wavefunctions between the shifted positions and the original position - wf_ratio = _compute_ratio_determinant_part_split_spin( - geminal_data=wavefunction_data.geminal_data, - A_old_inv=A_old_inv, - old_r_up_carts=r_up, - old_r_dn_carts=r_dn, - new_r_up_shifted=r_up_carts_shifted, - new_r_dn_shifted=r_dn_carts_shifted, - ) * _compute_ratio_Jastrow_part_rank1_update( - jastrow_data=wavefunction_data.jastrow_data, - old_r_up_carts=r_up, - old_r_dn_carts=r_dn, - new_r_up_carts_arr=r_up_carts_combined, - new_r_dn_carts_arr=r_dn_carts_combined, + # Evaluate the ratios of wavefunctions between the shifted positions and the original position. + # det_ratio (det_ratio zone) and jastrow_ratio (jastrow_ratio zone) are explicitly cast to + # the wf_ratio zone dtype to avoid relying on JAX implicit promotion. + dtype_wf_ratio_jnp = get_dtype_jnp("wf_ratio") + det_ratio = jnp.asarray( + _compute_ratio_determinant_part_split_spin( + geminal_data=wavefunction_data.geminal_data, + A_old_inv=A_old_inv, + old_r_up_carts=r_up, + old_r_dn_carts=r_dn, + new_r_up_shifted=r_up_carts_shifted, + new_r_dn_shifted=r_dn_carts_shifted, + ), + dtype=dtype_wf_ratio_jnp, + ) + jastrow_ratio = jnp.asarray( + _compute_ratio_Jastrow_part_rank1_update( + jastrow_data=wavefunction_data.jastrow_data, + old_r_up_carts=r_up, + old_r_dn_carts=r_dn, + new_r_up_carts_arr=r_up_carts_combined, + new_r_dn_carts_arr=r_dn_carts_combined, + ), + dtype=dtype_wf_ratio_jnp, ) + wf_ratio = det_ratio * jastrow_ratio # Compute the kinetic part elements elements_kinetic_part = -1.0 / (2.0 * alat**2) * wf_ratio @@ -1537,22 +1594,27 @@ def compute_nodal_distance( Returns: Scalar nodal distance value. """ - dtype = get_dtype("determinant") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + # r_*_carts forwarded unchanged (see ``evaluate_ln_wavefunction`` for rationale). + dtype_jnp = get_dtype_jnp("wf_eval") grad_J_up, grad_J_dn, _, _ = compute_grads_and_laplacian_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) grad_ln_D_up, grad_ln_D_dn, _, _ = compute_grads_and_laplacian_ln_Det( geminal_data=wavefunction_data.geminal_data, - r_up_carts=r_up, - r_dn_carts=r_dn, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, ) + # Cast jastrow_grad_lap / det_grad_lap zone values to wf_eval dtype. + grad_J_up = jnp.asarray(grad_J_up, dtype=dtype_jnp) + grad_J_dn = jnp.asarray(grad_J_dn, dtype=dtype_jnp) + grad_ln_D_up = jnp.asarray(grad_ln_D_up, dtype=dtype_jnp) + grad_ln_D_dn = jnp.asarray(grad_ln_D_dn, dtype=dtype_jnp) + grad_ln_Psi_up = grad_J_up + grad_ln_D_up # (n_up, 3) grad_ln_Psi_dn = grad_J_dn + grad_ln_D_dn # (n_dn, 3) @@ -1586,9 +1648,9 @@ def _compute_nodal_distance_debug( Returns: Scalar nodal distance value. """ - dtype = get_dtype("determinant") - r_up = jnp.asarray(r_up_carts, dtype=dtype) - r_dn = jnp.asarray(r_dn_carts, dtype=dtype) + dtype_jnp = get_dtype_jnp("wf_eval") + r_up = jnp.asarray(r_up_carts, dtype=dtype_jnp) + r_dn = jnp.asarray(r_dn_carts, dtype=dtype_jnp) Psi = evaluate_wavefunction(wavefunction_data, r_up, r_dn) diff --git a/jqmc_workflow/lrdmc_workflow.py b/jqmc_workflow/lrdmc_workflow.py index aac90004..09bd38ce 100644 --- a/jqmc_workflow/lrdmc_workflow.py +++ b/jqmc_workflow/lrdmc_workflow.py @@ -325,7 +325,7 @@ def __init__( max_continuation: int = 1, cleanup_patterns: Optional[list] = None, # -- [precision] section -- - precision_mode: Optional[str] = None, + precision_mode: str = "full", ): super().__init__(cleanup_patterns=cleanup_patterns) self.server_machine_name = server_machine_name @@ -462,9 +462,8 @@ def _generate_input( "control": control_ov, jt: section_ov, } - # Add [precision] section if configured - if self.precision_mode is not None: - overrides["precision"] = {"mode": self.precision_mode} + # Add [precision] section + overrides["precision"] = {"mode": self.precision_mode} generate_input_toml( job_type=jt, overrides=overrides, diff --git a/jqmc_workflow/mcmc_workflow.py b/jqmc_workflow/mcmc_workflow.py index fb49fe4c..780b7d64 100644 --- a/jqmc_workflow/mcmc_workflow.py +++ b/jqmc_workflow/mcmc_workflow.py @@ -248,7 +248,7 @@ def __init__( max_continuation: int = 1, cleanup_patterns: Optional[list] = None, # -- [precision] section -- - precision_mode: Optional[str] = None, + precision_mode: str = "full", ): super().__init__(cleanup_patterns=cleanup_patterns) self.server_machine_name = server_machine_name @@ -320,9 +320,8 @@ def _generate_input( "control": control_ov, "mcmc": mcmc_ov, } - # Add [precision] section if configured - if self.precision_mode is not None: - overrides["precision"] = {"mode": self.precision_mode} + # Add [precision] section + overrides["precision"] = {"mode": self.precision_mode} generate_input_toml( job_type="mcmc", overrides=overrides, diff --git a/jqmc_workflow/vmc_workflow.py b/jqmc_workflow/vmc_workflow.py index 403cb8dd..dc6f5470 100644 --- a/jqmc_workflow/vmc_workflow.py +++ b/jqmc_workflow/vmc_workflow.py @@ -312,7 +312,7 @@ def __init__( energy_slope_window_size: int = 5, cleanup_patterns: Optional[list] = None, # -- [precision] section -- - precision_mode: Optional[str] = None, + precision_mode: str = "full", ): super().__init__(cleanup_patterns=cleanup_patterns) self.server_machine_name = server_machine_name @@ -424,9 +424,8 @@ def _generate_input( "control": control_ov, "vmc": vmc_ov, } - # Add [precision] section if configured - if self.precision_mode is not None: - overrides["precision"] = {"mode": self.precision_mode} + # Add [precision] section + overrides["precision"] = {"mode": self.precision_mode} generate_input_toml( job_type="vmc", overrides=overrides, diff --git a/tests/test_AOs.py b/tests/test_AOs.py index c19d9e67..0e08b436 100755 --- a/tests/test_AOs.py +++ b/tests/test_AOs.py @@ -209,7 +209,7 @@ def Y_l_m_ref(l=0, m=0, r_cart_rel=None): r_y_rand = (r_cart_max - r_cart_min) * np.random.rand(num_samples) + r_cart_min r_z_rand = (r_cart_max - r_cart_min) * np.random.rand(num_samples) + r_cart_min - atol, rtol = get_tolerance("orb_eval", "strict") + atol, rtol = get_tolerance("ao_eval", "strict") for r_cart in zip(r_x_rand, r_y_rand, r_z_rand, strict=True): r_norm = LA.norm(np.array(R_cart) - np.array(r_cart)) r_cart_rel = np.array(r_cart) - np.array(R_cart) @@ -263,7 +263,7 @@ def test_solid_harmonics_debug_vs_production(): # print(f"batch_S_l_m.shape = {batch_S_l_m.shape}.") - atol, rtol = get_tolerance("orb_eval", "strict") + atol, rtol = get_tolerance("ao_eval", "strict") assert not np.any(np.isnan(np.asarray(S_l_m_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(S_l_m_jax))), "NaN detected in second argument" np.testing.assert_allclose(S_l_m_debug, S_l_m_jax, atol=atol, rtol=rtol) @@ -283,8 +283,8 @@ def test_AOs_sphe_debug_vs_production(): magnetic_quantum_numbers = [m for _, m in ml_list] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -317,7 +317,7 @@ def test_AOs_sphe_debug_vs_production(): ) aos_data.sanity_check() - atol, rtol = get_tolerance("orb_eval", "strict") + atol, rtol = get_tolerance("ao_eval", "strict") aos_jax = _compute_AOs_sphe(aos_data=aos_data, r_carts=r_carts) aos_debug = _compute_AOs_sphe_debug(aos_data=aos_data, r_carts=r_carts) @@ -336,8 +336,8 @@ def test_AOs_sphe_debug_vs_production(): magnetic_quantum_numbers = magnetic_quantum_numbers orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -406,8 +406,8 @@ def test_AOs_cart_debug_vs_production(): coefficients = [1.0] * num_ao orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) polynominal_order_x = tuple(polynominal_order_x) polynominal_order_y = tuple(polynominal_order_y) @@ -444,7 +444,7 @@ def test_AOs_cart_debug_vs_production(): ) aos_data.sanity_check() - atol, rtol = get_tolerance("orb_eval", "strict") + atol, rtol = get_tolerance("ao_eval", "strict") aos_jax = _compute_AOs_cart(aos_data=aos_data, r_carts=r_carts) aos_debug = _compute_AOs_cart_debug(aos_data=aos_data, r_carts=r_carts) @@ -470,8 +470,8 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_auto(): num_ao = 3 num_ao_prim = 3 orbital_indices = tuple(range(num_ao)) - exponents = tuple([0.8, 1.1, 0.6]) - coefficients = tuple([1.0, 0.7, 1.3]) + exponents = np.array([0.8, 1.1, 0.6], dtype=np.float64) + coefficients = np.array([1.0, 0.7, 1.3], dtype=np.float64) angular_momentums = tuple([0, 1, 2]) polynominal_order_x = tuple([0, 1, 2]) polynominal_order_y = tuple([0, 0, 0]) @@ -504,7 +504,7 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_auto(): gx_auto, gy_auto, gz_auto = _compute_AOs_grad_autodiff(aos_data=aos_data, r_carts=r_carts) gx_an, gy_an, gz_an = compute_AOs_grad(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("ao_grad_lap", "strict") assert not np.any(np.isnan(np.asarray(gx_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_auto))), "NaN detected in second argument" np.testing.assert_allclose(gx_an, gx_auto, atol=atol, rtol=rtol) @@ -533,8 +533,8 @@ def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): num_ao = 3 num_ao_prim = 3 orbital_indices = tuple(range(num_ao)) - exponents = tuple([1.2, 0.9, 0.7]) - coefficients = tuple([1.0, 0.8, 0.6]) + exponents = np.array([1.2, 0.9, 0.7], dtype=np.float64) + coefficients = np.array([1.0, 0.8, 0.6], dtype=np.float64) angular_momentums = tuple([0, 1, 2]) polynominal_order_x = tuple([0, 1, 2]) polynominal_order_y = tuple([0, 0, 0]) @@ -567,7 +567,7 @@ def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): gx_auto_cart, gy_auto_cart, gz_auto_cart = _compute_AOs_grad_autodiff(aos_data=aos_data_cart, r_carts=r_carts) gx_num_cart, gy_num_cart, gz_num_cart = _compute_AOs_grad_debug(aos_data=aos_data_cart, r_carts=r_carts) - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("ao_grad_lap", "loose") assert not np.any(np.isnan(np.asarray(gx_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(gx_auto_cart, gx_num_cart, atol=atol, rtol=rtol) @@ -595,8 +595,8 @@ def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): magnetic_quantum_numbers = [0, 0, 0, 0] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -658,8 +658,8 @@ def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): magnetic_quantum_numbers = [0, 0, 0, 0] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -721,8 +721,8 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_numerical(): num_ao = 3 num_ao_prim = 3 orbital_indices = tuple(range(num_ao)) - exponents = tuple([0.9, 1.3, 0.7]) - coefficients = tuple([1.0, 0.8, 1.2]) + exponents = np.array([0.9, 1.3, 0.7], dtype=np.float64) + coefficients = np.array([1.0, 0.8, 1.2], dtype=np.float64) angular_momentums = tuple([0, 1, 2]) polynominal_order_x = tuple([0, 1, 2]) polynominal_order_y = tuple([0, 0, 0]) @@ -755,7 +755,7 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_numerical(): gx_num_cart, gy_num_cart, gz_num_cart = _compute_AOs_grad_debug(aos_data=aos_data, r_carts=r_carts) gx_an_cart, gy_an_cart, gz_an_cart = compute_AOs_grad(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("ao_grad_lap", "loose") assert not np.any(np.isnan(np.asarray(gx_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(gx_an_cart, gx_num_cart, atol=atol, rtol=rtol) @@ -775,8 +775,8 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_numerical(): num_ao = 4 num_ao_prim = 5 orbital_indices = tuple([0, 1, 2, 2, 3]) - exponents = tuple([3.0, 1.6, 0.9, 0.9, 2.2]) - coefficients = tuple([1.0, 0.9, 1.1, 0.7, 1.0]) + exponents = np.array([3.0, 1.6, 0.9, 0.9, 2.2], dtype=np.float64) + coefficients = np.array([1.0, 0.9, 1.1, 0.7, 1.0], dtype=np.float64) angular_momentums = tuple([0, 1, 1, 2]) magnetic_quantum_numbers = tuple([0, -1, 1, 0]) @@ -833,8 +833,8 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_auto(): num_ao = 3 num_ao_prim = 3 orbital_indices = tuple(range(num_ao)) - exponents = tuple([0.9, 1.2, 0.7]) - coefficients = tuple([1.0, 0.8, 1.1]) + exponents = np.array([0.9, 1.2, 0.7], dtype=np.float64) + coefficients = np.array([1.0, 0.8, 1.1], dtype=np.float64) angular_momentums = tuple([0, 1, 2]) polynominal_order_x = tuple([0, 1, 2]) polynominal_order_y = tuple([0, 0, 0]) @@ -867,7 +867,7 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_auto(): lap_auto_cart = _compute_AOs_laplacian_autodiff(aos_data=aos_data, r_carts=r_carts) lap_an_cart = compute_AOs_laplacian(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("ao_grad_lap", "strict") assert not np.any(np.isnan(np.asarray(lap_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_auto_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_an_cart, lap_auto_cart, atol=atol, rtol=rtol) @@ -881,8 +881,8 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_auto(): num_ao = 4 num_ao_prim = 5 orbital_indices = tuple([0, 1, 2, 2, 3]) - exponents = tuple([3.0, 1.5, 0.8, 0.8, 2.2]) - coefficients = tuple([1.0, 0.9, 1.1, 0.7, 1.0]) + exponents = np.array([3.0, 1.5, 0.8, 0.8, 2.2], dtype=np.float64) + coefficients = np.array([1.0, 0.9, 1.1, 0.7, 1.0], dtype=np.float64) angular_momentums = tuple([0, 1, 1, 2]) magnetic_quantum_numbers = tuple([0, -1, 1, 0]) @@ -931,8 +931,8 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_numerical(): num_ao = 2 num_ao_prim = 3 orbital_indices = tuple([0, 0, 1]) - exponents = tuple([1.4, 0.9, 1.1]) - coefficients = tuple([1.0, 0.7, 0.9]) + exponents = np.array([1.4, 0.9, 1.1], dtype=np.float64) + coefficients = np.array([1.0, 0.7, 0.9], dtype=np.float64) angular_momentums = tuple([0, 1]) polynominal_order_x = tuple([0, 1]) polynominal_order_y = tuple([0, 0]) @@ -965,7 +965,7 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_numerical(): lap_num_cart = _compute_AOs_laplacian_debug(aos_data=aos_data, r_carts=r_carts) lap_an_cart = compute_AOs_laplacian(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("ao_grad_lap", "loose") assert not np.any(np.isnan(np.asarray(lap_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_an_cart, lap_num_cart, atol=atol, rtol=rtol) @@ -979,8 +979,8 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_numerical(): num_ao = 3 num_ao_prim = 4 orbital_indices = tuple([0, 1, 1, 2]) - exponents = tuple([2.0, 1.6, 1.1, 0.9]) - coefficients = tuple([1.0, 0.8, 1.2, 0.7]) + exponents = np.array([2.0, 1.6, 1.1, 0.9], dtype=np.float64) + coefficients = np.array([1.0, 0.8, 1.2, 0.7], dtype=np.float64) angular_momentums = tuple([0, 1, 1]) magnetic_quantum_numbers = tuple([0, 0, 1]) @@ -1053,8 +1053,8 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): num_ao=num_ao, num_ao_prim=num_ao_prim, orbital_indices=tuple(orbital_indices), - exponents=tuple(exponents), - coefficients=tuple(coefficients), + exponents=np.array(exponents, dtype=np.float64), + coefficients=np.array(coefficients, dtype=np.float64), angular_momentums=tuple(angular_momentums), polynominal_order_x=tuple(polynominal_order_x), polynominal_order_y=tuple(polynominal_order_y), @@ -1065,7 +1065,7 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): lap_num_cart = _compute_AOs_laplacian_autodiff(aos_data=aos_data, r_carts=r_carts) lap_auto_cart = _compute_AOs_laplacian_debug(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("ao_grad_lap", "loose") assert not np.any(np.isnan(np.asarray(lap_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_auto_cart, lap_num_cart, atol=atol, rtol=rtol) @@ -1087,8 +1087,8 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): magnetic_quantum_numbers = [0, 0, 0] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -1138,8 +1138,8 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): magnetic_quantum_numbers = [0, 1, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -1196,8 +1196,8 @@ def test_overlap_matrix_cart_analytic_vs_numerical_debug(): num_ao=2, num_ao_prim=2, orbital_indices=(0, 1), - exponents=(1.20, 1.20), - coefficients=(1.0, 1.0), + exponents=np.array([1.20, 1.20], dtype=np.float64), + coefficients=np.array([1.0, 1.0], dtype=np.float64), angular_momentums=(0, 0), polynominal_order_x=(0, 0), polynominal_order_y=(0, 0), @@ -1208,7 +1208,7 @@ def test_overlap_matrix_cart_analytic_vs_numerical_debug(): overlap_analytic = np.asarray(compute_overlap_matrix(aos_data=aos_data), dtype=np.float64) overlap_numerical = _compute_overlap_matrix_debug(aos_data=aos_data, num_grid_points=41, tail_tolerance=1.0e-12) - atol, rtol = get_tolerance("orb_eval", "strict") + atol, rtol = get_tolerance("ao_eval", "strict") assert not np.any(np.isnan(np.asarray(overlap_analytic))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(overlap_numerical))), "NaN detected in second argument" np.testing.assert_allclose(overlap_analytic, overlap_numerical, atol=atol, rtol=rtol) @@ -1240,8 +1240,8 @@ def test_overlap_matrix_sphe_analytic_vs_numerical_debug(): num_ao=2, num_ao_prim=2, orbital_indices=(0, 1), - exponents=(1.10, 1.10), - coefficients=(1.0, 1.0), + exponents=np.array([1.10, 1.10], dtype=np.float64), + coefficients=np.array([1.0, 1.0], dtype=np.float64), angular_momentums=(0, 0), magnetic_quantum_numbers=(0, 0), ) @@ -1250,8 +1250,8 @@ def test_overlap_matrix_sphe_analytic_vs_numerical_debug(): overlap_analytic = np.asarray(compute_overlap_matrix(aos_data=aos_data), dtype=np.float64) overlap_numerical = _compute_overlap_matrix_debug(aos_data=aos_data, num_grid_points=41, tail_tolerance=1.0e-12) - atol_l, rtol_l = get_tolerance("orb_eval", "loose") - atol_s, rtol_s = get_tolerance("orb_eval", "strict") + atol_l, rtol_l = get_tolerance("ao_eval", "loose") + atol_s, rtol_s = get_tolerance("ao_eval", "strict") assert not np.any(np.isnan(np.asarray(overlap_analytic))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(overlap_numerical))), "NaN detected in second argument" np.testing.assert_allclose(overlap_analytic, overlap_numerical, atol=atol_l, rtol=rtol_l) diff --git a/tests/test_MOs.py b/tests/test_MOs.py index 4afd12ed..4ab5b870 100755 --- a/tests/test_MOs.py +++ b/tests/test_MOs.py @@ -62,7 +62,7 @@ compute_MOs_grad, compute_MOs_laplacian, ) -from jqmc._precision import get_tolerance # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 from jqmc.structure import Structure_data # noqa: E402 # JAX float64 @@ -83,8 +83,8 @@ def test_MOs_comparing_jax_and_debug_implemenetations(): magnetic_quantum_numbers = [0, 0, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -125,7 +125,9 @@ def test_MOs_comparing_jax_and_debug_implemenetations(): mo_ans_all_debug = _compute_MOs_debug(mos_data=mos_data, r_carts=r_carts) - atol, rtol = get_tolerance("orb_eval", "strict") + # Path crosses ao_eval (fp32 in mixed) -> mo_eval (fp64); use the looser + # of the two so the test reflects the achievable agreement. + atol, rtol = get_tolerance_min(["ao_eval", "mo_eval"], "strict") assert not np.any(np.isnan(np.asarray(mo_ans_all_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_ans_all_jax))), "NaN detected in second argument" np.testing.assert_allclose(mo_ans_all_debug, mo_ans_all_jax, atol=atol, rtol=rtol) @@ -141,8 +143,8 @@ def test_MOs_comparing_jax_and_debug_implemenetations(): magnetic_quantum_numbers = [0, 1, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -204,8 +206,8 @@ def test_MOs_comparing_auto_and_numerical_grads(): magnetic_quantum_numbers = [0, 0, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -249,7 +251,7 @@ def test_MOs_comparing_auto_and_numerical_grads(): mo_matrix_grad_z_numerical, ) = _compute_MOs_grad_autodiff(mos_data=mos_data, r_carts=r_carts) - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("mo_grad_lap", "loose") assert not np.any(np.isnan(np.asarray(mo_matrix_grad_x_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_grad_x_numerical))), "NaN detected in second argument" np.testing.assert_allclose(mo_matrix_grad_x_auto, mo_matrix_grad_x_numerical, atol=atol, rtol=rtol) @@ -272,8 +274,8 @@ def test_MOs_comparing_auto_and_numerical_grads(): magnetic_quantum_numbers = [0, 1, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -345,8 +347,8 @@ def test_MOs_comparing_auto_and_numerical_laplacians(): magnetic_quantum_numbers = [0, 0, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -386,7 +388,7 @@ def test_MOs_comparing_auto_and_numerical_laplacians(): mo_matrix_laplacian_auto = _compute_MOs_laplacian_autodiff(mos_data=mos_data, r_carts=r_carts) - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("mo_grad_lap", "loose") assert not np.any(np.isnan(np.asarray(mo_matrix_laplacian_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_laplacian_numerical))), "NaN detected in second argument" np.testing.assert_allclose( @@ -413,8 +415,8 @@ def test_MOs_comparing_analytic_and_auto_grads(): magnetic_quantum_numbers = [0, 1, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -452,7 +454,8 @@ def test_MOs_comparing_analytic_and_auto_grads(): grad_x_auto, grad_y_auto, grad_z_auto = _compute_MOs_grad_autodiff(mos_data=mos_data, r_carts=r_carts) - atol, rtol = get_tolerance("kinetic", "strict") + # Path crosses ao_grad_lap (fp32 in mixed) -> mo_grad_lap (fp64); use min. + atol, rtol = get_tolerance_min(["ao_grad_lap", "mo_grad_lap"], "strict") assert not np.any(np.isnan(np.asarray(grad_x_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_x_auto))), "NaN detected in second argument" np.testing.assert_allclose(grad_x_an, grad_x_auto, atol=atol, rtol=rtol) @@ -480,8 +483,8 @@ def test_MOs_comparing_analytic_and_auto_laplacians(): magnetic_quantum_numbers = [0, 1, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -519,7 +522,8 @@ def test_MOs_comparing_analytic_and_auto_laplacians(): mo_lap_auto = _compute_MOs_laplacian_autodiff(mos_data=mos_data, r_carts=r_carts) - atol, rtol = get_tolerance("kinetic", "strict") + # Path crosses ao_grad_lap (fp32 in mixed) -> mo_grad_lap (fp64); use min. + atol, rtol = get_tolerance_min(["ao_grad_lap", "mo_grad_lap"], "strict") assert not np.any(np.isnan(np.asarray(mo_lap_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_lap_auto))), "NaN detected in second argument" np.testing.assert_allclose(mo_lap_an, mo_lap_auto, atol=atol, rtol=rtol) @@ -574,8 +578,8 @@ def test_MOs_sphe_to_cart(): num_ao=num_ao, num_ao_prim=num_ao_prim, orbital_indices=tuple(orbital_indices), - exponents=tuple(exponents), - coefficients=tuple(coefficients), + exponents=np.array(exponents, dtype=np.float64), + coefficients=np.array(coefficients, dtype=np.float64), angular_momentums=tuple(angular_momentums), magnetic_quantum_numbers=tuple(magnetic_quantum_numbers), ) @@ -590,7 +594,8 @@ def test_MOs_sphe_to_cart(): mo_sphe = compute_MOs(mos_data=mos_sphe, r_carts=r_carts) mo_cart = compute_MOs(mos_data=mos_cart, r_carts=r_carts) - atol, rtol = get_tolerance("orb_eval", "strict") + # Path crosses ao_eval (fp32 in mixed) -> mo_eval; use min for value cmp. + atol, rtol = get_tolerance_min(["ao_eval", "mo_eval"], "strict") assert not np.any(np.isnan(np.asarray(mo_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_sphe))), "NaN detected in second argument" np.testing.assert_allclose(mo_cart, mo_sphe, atol=atol, rtol=rtol) @@ -598,17 +603,19 @@ def test_MOs_sphe_to_cart(): grad_sphe = compute_MOs_grad(mos_data=mos_sphe, r_carts=r_carts) grad_cart = compute_MOs_grad(mos_data=mos_cart, r_carts=r_carts) + # grad/lap path crosses ao_grad_lap (fp32 in mixed) -> mo_grad_lap. + atol_gl, rtol_gl = get_tolerance_min(["ao_grad_lap", "mo_grad_lap"], "strict") for g_cart, g_sphe in zip(grad_cart, grad_sphe, strict=True): assert not np.any(np.isnan(np.asarray(g_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(g_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(g_cart, g_sphe, atol=atol, rtol=rtol) + np.testing.assert_allclose(g_cart, g_sphe, atol=atol_gl, rtol=rtol_gl) lap_sphe = compute_MOs_laplacian(mos_data=mos_sphe, r_carts=r_carts) lap_cart = compute_MOs_laplacian(mos_data=mos_cart, r_carts=r_carts) assert not np.any(np.isnan(np.asarray(lap_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(lap_cart, lap_sphe, atol=atol, rtol=rtol) + np.testing.assert_allclose(lap_cart, lap_sphe, atol=atol_gl, rtol=rtol_gl) jax.clear_caches() @@ -674,8 +681,8 @@ def test_MOs_cart_to_sphe(): num_ao=num_ao, num_ao_prim=num_ao_prim, orbital_indices=tuple(orbital_indices), - exponents=tuple(exponents), - coefficients=tuple(coefficients), + exponents=np.array(exponents, dtype=np.float64), + coefficients=np.array(coefficients, dtype=np.float64), angular_momentums=tuple(angular_momentums), polynominal_order_x=tuple(polynominal_order_x), polynominal_order_y=tuple(polynominal_order_y), @@ -692,7 +699,8 @@ def test_MOs_cart_to_sphe(): mo_cart = compute_MOs(mos_data=mos_cart, r_carts=r_carts) mo_sphe = compute_MOs(mos_data=mos_sphe, r_carts=r_carts) - atol, rtol = get_tolerance("orb_eval", "strict") + # Path crosses ao_eval (fp32 in mixed) -> mo_eval; use min for value cmp. + atol, rtol = get_tolerance_min(["ao_eval", "mo_eval"], "strict") assert not np.any(np.isnan(np.asarray(mo_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_cart))), "NaN detected in second argument" np.testing.assert_allclose(mo_sphe, mo_cart, atol=atol, rtol=rtol) @@ -700,17 +708,19 @@ def test_MOs_cart_to_sphe(): grad_cart = compute_MOs_grad(mos_data=mos_cart, r_carts=r_carts) grad_sphe = compute_MOs_grad(mos_data=mos_sphe, r_carts=r_carts) + # grad/lap path crosses ao_grad_lap (fp32 in mixed) -> mo_grad_lap. + atol_gl, rtol_gl = get_tolerance_min(["ao_grad_lap", "mo_grad_lap"], "strict") for g_cart, g_sphe in zip(grad_cart, grad_sphe, strict=True): assert not np.any(np.isnan(np.asarray(g_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(g_cart))), "NaN detected in second argument" - np.testing.assert_allclose(g_sphe, g_cart, atol=atol, rtol=rtol) + np.testing.assert_allclose(g_sphe, g_cart, atol=atol_gl, rtol=rtol_gl) lap_cart = compute_MOs_laplacian(mos_data=mos_cart, r_carts=r_carts) lap_sphe = compute_MOs_laplacian(mos_data=mos_sphe, r_carts=r_carts) assert not np.any(np.isnan(np.asarray(lap_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_cart))), "NaN detected in second argument" - np.testing.assert_allclose(lap_sphe, lap_cart, atol=atol, rtol=rtol) + np.testing.assert_allclose(lap_sphe, lap_cart, atol=atol_gl, rtol=rtol_gl) jax.clear_caches() diff --git a/tests/test_ao_basis_optimization.py b/tests/test_ao_basis_optimization.py index 586f3688..d6ab1465 100644 --- a/tests/test_ao_basis_optimization.py +++ b/tests/test_ao_basis_optimization.py @@ -84,11 +84,16 @@ def _random_electron_coords(structure_data, coulomb_potential_data, geminal_data @pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvdz_cart.h5", "H2_ae_ccpvdz_sphe.h5"]) -def test_exponents_coefficients_are_jax_arrays(trexio_file): - """After Phase 1, exponents/coefficients should be jax.Array.""" +def test_exponents_coefficients_storage_is_numpy(trexio_file): + """Storage fields exponents/coefficients are np.ndarray[float64] (jnp view via _*_jnp).""" structure_data, aos_data, *_ = _load_trexio(trexio_file) - assert isinstance(aos_data.exponents, jax.Array), f"exponents type: {type(aos_data.exponents)}" - assert isinstance(aos_data.coefficients, jax.Array), f"coefficients type: {type(aos_data.coefficients)}" + assert isinstance(aos_data.exponents, np.ndarray), f"exponents type: {type(aos_data.exponents)}" + assert aos_data.exponents.dtype == np.float64, f"exponents dtype: {aos_data.exponents.dtype}" + assert isinstance(aos_data.coefficients, np.ndarray), f"coefficients type: {type(aos_data.coefficients)}" + assert aos_data.coefficients.dtype == np.float64, f"coefficients dtype: {aos_data.coefficients.dtype}" + # The jnp accessor returns jax.Array + assert isinstance(aos_data._exponents_jnp, jax.Array) + assert isinstance(aos_data._coefficients_jnp, jax.Array) # ============================================================ @@ -132,15 +137,15 @@ def test_j3_with_updated_ao_exponents(): npt.assert_allclose( np.array(j3_new.ao_exponents), np.array(new_exp), - atol=get_tolerance("orb_eval", "strict")[0], - rtol=get_tolerance("orb_eval", "strict")[1], + atol=get_tolerance("ao_eval", "strict")[0], + rtol=get_tolerance("ao_eval", "strict")[1], ) # Original should be unchanged npt.assert_allclose( np.array(j3.ao_exponents), np.array(aos_data.exponents), - atol=get_tolerance("orb_eval", "strict")[0], - rtol=get_tolerance("orb_eval", "strict")[1], + atol=get_tolerance("ao_eval", "strict")[0], + rtol=get_tolerance("ao_eval", "strict")[1], ) @@ -166,14 +171,14 @@ def test_geminal_ao_properties(): npt.assert_allclose( np.array(exp_up), np.array(exp_up_ao), - atol=get_tolerance("orb_eval", "strict")[0], - rtol=get_tolerance("orb_eval", "strict")[1], + atol=get_tolerance("ao_eval", "strict")[0], + rtol=get_tolerance("ao_eval", "strict")[1], ) npt.assert_allclose( np.array(exp_dn), np.array(exp_dn_ao), - atol=get_tolerance("orb_eval", "strict")[0], - rtol=get_tolerance("orb_eval", "strict")[1], + atol=get_tolerance("ao_eval", "strict")[0], + rtol=get_tolerance("ao_eval", "strict")[1], ) @@ -194,14 +199,14 @@ def test_geminal_with_updated_ao_exponents(): npt.assert_allclose( np.array(geminal_new.ao_exponents_up), np.array(new_exp_up), - atol=get_tolerance("orb_eval", "strict")[0], - rtol=get_tolerance("orb_eval", "strict")[1], + atol=get_tolerance("ao_eval", "strict")[0], + rtol=get_tolerance("ao_eval", "strict")[1], ) npt.assert_allclose( np.array(geminal_new.ao_exponents_dn), np.array(new_exp_dn), - atol=get_tolerance("orb_eval", "strict")[0], - rtol=get_tolerance("orb_eval", "strict")[1], + atol=get_tolerance("ao_eval", "strict")[0], + rtol=get_tolerance("ao_eval", "strict")[1], ) # Lambda matrix should be unchanged npt.assert_array_equal(np.array(geminal_new.lambda_matrix), np.array(geminal_ao.lambda_matrix)) @@ -370,8 +375,8 @@ def test_get_variational_blocks_basis_flags(): npt.assert_allclose( symmetrized, np.asarray(aos_data.exponents), - atol=get_tolerance("orb_eval", "strict")[0], - rtol=get_tolerance("orb_eval", "strict")[1], + atol=get_tolerance("ao_eval", "strict")[0], + rtol=get_tolerance("ao_eval", "strict")[1], ) @@ -402,8 +407,8 @@ def test_apply_block_update_j3_basis(): npt.assert_allclose( np.array(jastrow_new.jastrow_three_body_data.ao_exponents), new_exp, - atol=get_tolerance("orb_eval", "strict")[0], - rtol=get_tolerance("orb_eval", "strict")[1], + atol=get_tolerance("ao_eval", "strict")[0], + rtol=get_tolerance("ao_eval", "strict")[1], ) @@ -430,14 +435,14 @@ def test_apply_block_update_geminal_basis(): npt.assert_allclose( np.array(geminal_new.ao_exponents_up), new_exp_up, - atol=get_tolerance("orb_eval", "strict")[0], - rtol=get_tolerance("orb_eval", "strict")[1], + atol=get_tolerance("ao_eval", "strict")[0], + rtol=get_tolerance("ao_eval", "strict")[1], ) npt.assert_allclose( np.array(geminal_new.ao_exponents_dn), new_exp_dn, - atol=get_tolerance("orb_eval", "strict")[0], - rtol=get_tolerance("orb_eval", "strict")[1], + atol=get_tolerance("ao_eval", "strict")[0], + rtol=get_tolerance("ao_eval", "strict")[1], ) @@ -611,7 +616,7 @@ def test_shell_symmetrize_j3_basis(): spm = ShellPrimMap.from_aos_data(aos_data) expected = spm.symmetrize(perturbed) npt.assert_allclose( - result, expected, atol=get_tolerance("orb_eval", "strict")[0], rtol=get_tolerance("orb_eval", "strict")[1] + result, expected, atol=get_tolerance("ao_eval", "strict")[0], rtol=get_tolerance("ao_eval", "strict")[1] ) @@ -656,7 +661,7 @@ def test_shell_symmetrize_geminal_basis(): ) expected = spm.symmetrize(perturbed) npt.assert_allclose( - result, expected, atol=get_tolerance("orb_eval", "strict")[0], rtol=get_tolerance("orb_eval", "strict")[1] + result, expected, atol=get_tolerance("ao_eval", "strict")[0], rtol=get_tolerance("ao_eval", "strict")[1] ) diff --git a/tests/test_determinant.py b/tests/test_determinant.py index dc1cce5c..7655c790 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -46,7 +46,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 from jqmc.atomic_orbital import AOs_sphe_data, compute_overlap_matrix # noqa: E402 from jqmc.determinant import ( # noqa: E402 Geminal_data, @@ -156,8 +156,8 @@ def test_convert_from_MOs_to_AOs_closed_shell(trexio_file: str): r_up_carts = np.array(r_up_carts).reshape(-1, 3) r_dn_carts = np.array(r_dn_carts).reshape(-1, 3) - atol_g, rtol_g = get_tolerance("geminal", "strict") - atol_d, rtol_d = get_tolerance("determinant", "strict") + atol_g, rtol_g = get_tolerance("det_eval", "strict") + atol_d, rtol_d = get_tolerance("det_eval", "strict") geminal_mo_debug = _compute_geminal_all_elements_debug( geminal_data=geminal_mo_data, @@ -300,8 +300,8 @@ def _build_sphe_aos_l_le6(rng: np.random.Generator) -> AOs_sphe_data: num_ao=len(angular_momentums), num_ao_prim=len(exponents), orbital_indices=tuple(orbital_indices), - exponents=tuple(exponents), - coefficients=tuple(coefficients), + exponents=np.array(exponents, dtype=np.float64), + coefficients=np.array(coefficients, dtype=np.float64), angular_momentums=tuple(angular_momentums), magnetic_quantum_numbers=tuple(magnetic_quantum_numbers), ) @@ -309,7 +309,9 @@ def _build_sphe_aos_l_le6(rng: np.random.Generator) -> AOs_sphe_data: def test_geminal_sphe_to_cart_AOs_data(): """Round-trip AOs l<=6: spherical→Cartesian keeps geminal values/grads.""" - atol_c, rtol_c = get_tolerance("geminal", "strict") + # Comparison crosses ao_eval/det_eval (values) and ao_grad_lap/det_grad_lap (grads); + # achievable agreement is bounded by the loosest zone on the path. + atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad_lap", "det_grad_lap"), "strict") rng = np.random.default_rng(321) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -347,7 +349,9 @@ def test_geminal_sphe_to_cart_AOs_data(): def test_geminal_cart_to_sphe_AOs_data(): """Round-trip AOs l<=6: Cartesian→spherical keeps geminal values/grads.""" - atol_c, rtol_c = get_tolerance("geminal", "strict") + # Comparison crosses ao_eval/det_eval (values) and ao_grad_lap/det_grad_lap (grads); + # achievable agreement is bounded by the loosest zone on the path. + atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad_lap", "det_grad_lap"), "strict") rng = np.random.default_rng(654) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -387,7 +391,11 @@ def test_geminal_cart_to_sphe_AOs_data(): def test_geminal_sphe_to_cart_MOs_data(): """Round-trip MOs built on l<=6 AOs: spherical→Cartesian keeps geminal values/grads.""" - atol_c, rtol_c = get_tolerance("geminal", "strict") + # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad_lap/mo_grad_lap/det_grad_lap (grads); + # achievable agreement is bounded by the loosest zone on the path. + atol_c, rtol_c = get_tolerance_min( + ("ao_eval", "mo_eval", "det_eval", "ao_grad_lap", "mo_grad_lap", "det_grad_lap"), "strict" + ) rng = np.random.default_rng(777) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -429,7 +437,11 @@ def test_geminal_sphe_to_cart_MOs_data(): def test_geminal_cart_to_sphe_MOs_data(): """Round-trip MOs l<=6: Cartesian→spherical keeps geminal values/grads.""" - atol_c, rtol_c = get_tolerance("geminal", "strict") + # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad_lap/mo_grad_lap/det_grad_lap (grads); + # achievable agreement is bounded by the loosest zone on the path. + atol_c, rtol_c = get_tolerance_min( + ("ao_eval", "mo_eval", "det_eval", "ao_grad_lap", "mo_grad_lap", "det_grad_lap"), "strict" + ) rng = np.random.default_rng(888) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -485,8 +497,8 @@ def _build_small_sphe_aos_for_conversion() -> AOs_sphe_data: num_ao=4, num_ao_prim=4, orbital_indices=(0, 1, 2, 3), - exponents=(1.20, 1.00, 1.00, 1.00), - coefficients=(1.0, 1.0, 1.0, 1.0), + exponents=np.array([1.20, 1.00, 1.00, 1.00], dtype=np.float64), + coefficients=np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float64), angular_momentums=(0, 1, 1, 1), magnetic_quantum_numbers=(0, -1, 0, 1), ) @@ -494,7 +506,7 @@ def _build_small_sphe_aos_for_conversion() -> AOs_sphe_data: def test_convert_from_AOs_to_MOs_full_projection_closed_shell(): """AO->MO (all eigenvectors) followed by MO->AO recovers the AO lambda matrix.""" - atol_c, rtol_c = get_tolerance("determinant", "strict") + atol_c, rtol_c = get_tolerance("det_eval", "strict") rng = np.random.default_rng(1234) aos_data = _build_small_sphe_aos_for_conversion() aos_data.sanity_check() @@ -528,7 +540,7 @@ def test_convert_from_AOs_to_MOs_full_projection_closed_shell(): def test_convert_from_AOs_to_MOs_full_projection_open_shell(): """AO->MO (all eigenvectors) round-trip recovers AO lambda matrix for open-shell case.""" - atol_c, rtol_c = get_tolerance("determinant", "strict") + atol_c, rtol_c = get_tolerance("det_eval", "strict") rng = np.random.default_rng(1334) aos_data = _build_small_sphe_aos_for_conversion() aos_data.sanity_check() @@ -629,7 +641,7 @@ def test_convert_from_AOs_to_MOs_truncated_mode_open_shell(): def test_apply_ao_projected_paired_update_and_reproject_fixed_num_dn(): """AO-corrected paired update is applied then reprojected with fixed N=num_electron_dn.""" - atol_c, rtol_c = get_tolerance("determinant", "strict") + atol_c, rtol_c = get_tolerance("det_eval", "strict") rng = np.random.default_rng(97531) aos_data = _build_small_sphe_aos_for_conversion() aos_data.sanity_check() @@ -806,7 +818,7 @@ def test_grads_and_laplacian_fast_update(trexio_file: str): r_dn_carts=r_dn_carts, ) - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("det_grad_lap", "strict") assert not np.any(np.isnan(np.asarray(grad_up_fast))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_up_debug))), "NaN detected in second argument" np.testing.assert_allclose(grad_up_fast, grad_up_debug, atol=atol, rtol=rtol) @@ -904,7 +916,7 @@ def test_comparing_AS_regularization(trexio_file: str): R_AS_jax = compute_AS_regularization_factor(geminal_data=geminal_mo_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - atol, rtol = get_tolerance("determinant", "strict") + atol, rtol = get_tolerance("det_eval", "strict") assert not np.any(np.isnan(np.asarray(R_AS_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(R_AS_jax))), "NaN detected in second argument" np.testing.assert_allclose(R_AS_debug, R_AS_jax, atol=atol, rtol=rtol) @@ -1015,7 +1027,7 @@ def test_one_row_or_one_column_update(trexio_file: str): ) # --- Numerical consistency asserts (no shape checks) --- - atol, rtol = get_tolerance("geminal", "strict") + atol, rtol = get_tolerance("det_eval", "strict") # up-one-row must equal the i-th row of the full geminal assert not np.any(np.isnan(np.asarray(np.asarray(geminal_mo_up_one_row).ravel()))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(geminal_mo[i_up, :])))), "NaN detected in second argument" @@ -1152,7 +1164,7 @@ def test_numerial_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): r_dn_carts=r_dn_carts, ) - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("det_grad_lap", "loose") assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_numerical)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( @@ -1284,7 +1296,7 @@ def test_analytic_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): r_dn_carts=r_dn_carts, ) - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("det_grad_lap", "strict") assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_analytic)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( @@ -1405,7 +1417,7 @@ def test_ratio_determinant_rank1_update(pattern: str): new_r_dn_carts_arr=new_r_dn_carts_arr, ) - atol, rtol = get_tolerance("determinant", "strict") + atol, rtol = get_tolerance("det_eval", "strict") atol_c, rtol_c = atol, rtol assert not np.any(np.isnan(np.asarray(np.asarray(ratio_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(ratio_rank1)))), "NaN detected in second argument" @@ -1430,7 +1442,7 @@ def test_compute_ln_det_geminal_all_elements_fast_forward(trexio_file): n_up = geminal_data.num_electron_up n_dn = geminal_data.num_electron_dn - atol, rtol = get_tolerance("determinant", "strict") + atol, rtol = get_tolerance("det_eval", "strict") for _ in range(10): r_up = jnp.array(rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) @@ -1467,7 +1479,11 @@ def test_compute_ln_det_geminal_all_elements_fast_backward(trexio_file): grad_ref_fn = jax.grad(compute_ln_det_geminal_all_elements, argnums=0) grad_fast_fn = jax.grad(compute_ln_det_geminal_all_elements_fast, argnums=0) - atol, rtol = get_tolerance("determinant", "strict") + # Backward-AD comparison: ref uses SVD-pseudoinverse custom VJP, fast uses + # caller-supplied G_inv; both VJPs propagate through compute_geminal_all_elements + # which crosses ao_eval/mo_eval/det_eval. Tolerance is bounded by the + # loosest zone on the path. + atol, rtol = get_tolerance_min(("ao_eval", "mo_eval", "det_eval"), "strict") for _ in range(10): r_up = jnp.array(rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) diff --git a/tests/test_hamiltonian.py b/tests/test_hamiltonian.py index cbf87b41..c76c1254 100644 --- a/tests/test_hamiltonian.py +++ b/tests/test_hamiltonian.py @@ -215,7 +215,7 @@ def test_compute_local_energy_fast(trexio_file): n_up = geminal_data.num_electron_up n_dn = geminal_data.num_electron_dn - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("local_energy", "strict") for _ in range(10): r_up = jnp.array(first_nucleus + rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(first_nucleus + rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) @@ -246,7 +246,7 @@ def _compare_grad_leaves( ): """Flatten two pytrees and compare every leaf.""" if atol is None or rtol is None: - _atol, _rtol = get_tolerance_min(["geminal", "jastrow"], "strict") + _atol, _rtol = get_tolerance_min(["det_eval", "jastrow_eval"], "strict") if atol is None: atol = _atol if rtol is None: @@ -297,7 +297,7 @@ def test_grad_compute_local_energy(trexio_file): r_dn = jnp.array(first_nucleus + rng.standard_normal((n_dn, 3)) * 0.5, dtype=jnp.float64) # Sanity: both forward values must agree. - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("local_energy", "strict") e_auto = float(_compute_local_energy_auto(hamiltonian_data, r_up, r_dn, RT)) e_custom = float(compute_local_energy(hamiltonian_data, r_up, r_dn, RT)) np.testing.assert_allclose( @@ -314,7 +314,7 @@ def test_grad_compute_local_energy(trexio_file): grad_auto = jax.grad(_compute_local_energy_auto, argnums=0)(hamiltonian_data, r_up, r_dn, RT) grad_custom = jax.grad(compute_local_energy, argnums=0)(hamiltonian_data, r_up, r_dn, RT) - atol_grad, rtol_grad = get_tolerance_min(["geminal", "jastrow"], "strict") + atol_grad, rtol_grad = get_tolerance_min(["det_eval", "jastrow_eval"], "strict") _compare_grad_leaves( grad_auto, grad_custom, diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index 63e0f74d..8fb286fb 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -80,7 +80,7 @@ @pytest.mark.parametrize("j1b_type", ["exp", "pade"]) def test_Jastrow_onebody_part(j1b_type): """Test the one-body Jastrow factor, comparing the debug and JAX implementations.""" - atol, rtol = get_tolerance("jastrow", "strict") + atol, rtol = get_tolerance("jastrow_eval", "strict") num_r_up_cart_samples = 8 num_r_dn_cart_samples = 4 num_R_cart_samples = 6 @@ -129,7 +129,7 @@ def test_Jastrow_onebody_part(j1b_type): @pytest.mark.parametrize("j1b_type", ["exp", "pade"]) def test_numerical_and_auto_grads_Jastrow_onebody_part(j1b_type): """Test numerical and JAX grads of the one-body Jastrow factor.""" - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("jastrow_grad_lap", "loose") num_r_up_cart_samples = 6 num_r_dn_cart_samples = 3 num_R_cart_samples = 5 @@ -193,7 +193,7 @@ def test_numerical_and_auto_grads_Jastrow_onebody_part(j1b_type): @pytest.mark.parametrize("j1b_type", ["exp", "pade"]) def test_analytical_and_auto_grads_Jastrow_onebody_part(j1b_type): """Analytic vs auto-diff gradients/laplacian for one-body Jastrow.""" - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("jastrow_grad_lap", "strict") num_r_up_cart_samples = 5 num_r_dn_cart_samples = 4 num_R_cart_samples = 4 @@ -247,7 +247,7 @@ def test_analytical_and_auto_grads_Jastrow_onebody_part(j1b_type): @pytest.mark.parametrize("j2b_type", ["pade", "exp"]) def test_Jastrow_twobody_part(j2b_type): """Test the two-body Jastrow factor, comparing the debug and JAX implementations.""" - atol, rtol = get_tolerance("jastrow", "strict") + atol, rtol = get_tolerance("jastrow_eval", "strict") num_r_up_cart_samples = 5 num_r_dn_cart_samples = 2 @@ -279,8 +279,8 @@ def test_Jastrow_twobody_part(j2b_type): @pytest.mark.parametrize("j2b_type", ["pade", "exp"]) def test_numerical_and_auto_grads_Jastrow_twobody_part(j2b_type): """Test numerical and JAX grads of the two-body Jastrow factor, comparing the debug and JAX implementations.""" - atol_s, rtol_s = get_tolerance("jastrow", "strict") - atol_l, rtol_l = get_tolerance("kinetic", "loose") + atol_s, rtol_s = get_tolerance("jastrow_eval", "strict") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "loose") num_r_up_cart_samples = 5 num_r_dn_cart_samples = 2 @@ -359,7 +359,7 @@ def test_numerical_and_auto_grads_Jastrow_twobody_part(j2b_type): @pytest.mark.parametrize("j2b_type", ["pade", "exp"]) def test_analytic_and_auto_grads_Jastrow_twobody_part(j2b_type): """Analytic vs auto-diff gradients/laplacian for two-body Jastrow.""" - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("jastrow_grad_lap", "strict") num_r_up_cart_samples = 5 num_r_dn_cart_samples = 2 @@ -422,7 +422,7 @@ def test_analytic_and_auto_grads_Jastrow_twobody_part(j2b_type): def test_Jastrow_threebody_part_with_AOs_data(): """Test the three-body Jastrow factor, comparing the debug and JAX implementations, using AOs data.""" - atol, rtol = get_tolerance("jastrow", "strict") + atol, rtol = get_tolerance("jastrow_eval", "strict") num_r_up_cart_samples = 4 num_r_dn_cart_samples = 2 num_R_cart_samples = 6 @@ -435,8 +435,8 @@ def test_Jastrow_threebody_part_with_AOs_data(): magnetic_quantum_numbers = [0, 0, 0, 0, +1, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -496,7 +496,7 @@ def test_Jastrow_threebody_part_with_AOs_data(): def test_Jastrow_threebody_part_with_MOs_data(): """Test the three-body Jastrow factor, comparing the debug and JAX implementations, using MOs data.""" - atol, rtol = get_tolerance("jastrow", "strict") + atol, rtol = get_tolerance("jastrow_eval", "strict") num_el = 10 num_mo = 5 num_ao = 3 @@ -508,8 +508,8 @@ def test_Jastrow_threebody_part_with_MOs_data(): magnetic_quantum_numbers = [0, 0, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -575,7 +575,7 @@ def test_Jastrow_threebody_part_with_MOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_sphe_to_cart_AOs_data(): """Round-trip AOs l<=6: spherical→Cartesian keeps J3 values/grads.""" - atol, rtol = get_tolerance("jastrow", "strict") + atol, rtol = get_tolerance("jastrow_eval", "strict") rng = np.random.default_rng(321) nucleus_index: list[int] = [] @@ -612,8 +612,8 @@ def test_Jastrow_threebody_part_sphe_to_cart_AOs_data(): num_ao=len(angular_momentums), num_ao_prim=len(exponents), orbital_indices=tuple(orbital_indices), - exponents=tuple(exponents), - coefficients=tuple(coefficients), + exponents=np.array(exponents, dtype=np.float64), + coefficients=np.array(coefficients, dtype=np.float64), angular_momentums=tuple(angular_momentums), magnetic_quantum_numbers=tuple(magnetic_quantum_numbers), ) @@ -645,7 +645,7 @@ def test_Jastrow_threebody_part_sphe_to_cart_AOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_cart_to_sphe_AOs_data(): """Round-trip AOs l<=6: Cartesian→spherical keeps J3 values/grads.""" - atol, rtol = get_tolerance("jastrow", "strict") + atol, rtol = get_tolerance("jastrow_eval", "strict") rng = np.random.default_rng(654) nucleus_index: list[int] = [] @@ -682,8 +682,8 @@ def test_Jastrow_threebody_part_cart_to_sphe_AOs_data(): num_ao=len(angular_momentums), num_ao_prim=len(exponents), orbital_indices=tuple(orbital_indices), - exponents=tuple(exponents), - coefficients=tuple(coefficients), + exponents=np.array(exponents, dtype=np.float64), + coefficients=np.array(coefficients, dtype=np.float64), angular_momentums=tuple(angular_momentums), magnetic_quantum_numbers=tuple(magnetic_quantum_numbers), ) @@ -715,7 +715,7 @@ def test_Jastrow_threebody_part_cart_to_sphe_AOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_sphe_to_cart_MOs_data(): """Round-trip MOs built on l<=6 AOs: spherical→Cartesian keeps J3 values/grads.""" - atol, rtol = get_tolerance("jastrow", "strict") + atol, rtol = get_tolerance("jastrow_eval", "strict") rng = np.random.default_rng(777) nucleus_index: list[int] = [] @@ -755,8 +755,8 @@ def test_Jastrow_threebody_part_sphe_to_cart_MOs_data(): num_ao=num_ao, num_ao_prim=len(exponents), orbital_indices=tuple(orbital_indices), - exponents=tuple(exponents), - coefficients=tuple(coefficients), + exponents=np.array(exponents, dtype=np.float64), + coefficients=np.array(coefficients, dtype=np.float64), angular_momentums=tuple(angular_momentums), magnetic_quantum_numbers=tuple(magnetic_quantum_numbers), ) @@ -792,7 +792,7 @@ def test_Jastrow_threebody_part_sphe_to_cart_MOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_cart_to_sphe_MOs_data(): """Round-trip MOs l<=6: Cartesian→spherical keeps J3 values/grads.""" - atol, rtol = get_tolerance("jastrow", "strict") + atol, rtol = get_tolerance("jastrow_eval", "strict") rng = np.random.default_rng(888) nucleus_index: list[int] = [] @@ -832,8 +832,8 @@ def test_Jastrow_threebody_part_cart_to_sphe_MOs_data(): num_ao=num_ao, num_ao_prim=len(exponents), orbital_indices=tuple(orbital_indices), - exponents=tuple(exponents), - coefficients=tuple(coefficients), + exponents=np.array(exponents, dtype=np.float64), + coefficients=np.array(coefficients, dtype=np.float64), angular_momentums=tuple(angular_momentums), magnetic_quantum_numbers=tuple(magnetic_quantum_numbers), ) @@ -870,8 +870,8 @@ def test_Jastrow_threebody_part_cart_to_sphe_MOs_data(): @pytest.mark.numerical_diff def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): """Test numerical and JAX grads of the three-body Jastrow factor, comparing the debug and JAX implementations, using AOs data.""" - atol_s, rtol_s = get_tolerance("jastrow", "strict") - atol_l, rtol_l = get_tolerance("kinetic", "loose") + atol_s, rtol_s = get_tolerance("jastrow_eval", "strict") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "loose") num_r_up_cart_samples = 4 num_r_dn_cart_samples = 2 num_R_cart_samples = 6 @@ -884,8 +884,8 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): magnetic_quantum_numbers = [0, 0, 0, 0, +1, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -985,8 +985,8 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): @pytest.mark.numerical_diff def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): """Test numerical and JAX grads of the three-body Jastrow factor, comparing the debug and JAX implementations, using MOs data.""" - atol_s, rtol_s = get_tolerance("jastrow", "strict") - atol_l, rtol_l = get_tolerance("kinetic", "loose") + atol_s, rtol_s = get_tolerance("jastrow_eval", "strict") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "loose") num_el = 10 num_mo = 5 num_ao = 3 @@ -998,8 +998,8 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): magnetic_quantum_numbers = [0, 0, -1] orbital_indices = tuple(orbital_indices) - exponents = tuple(exponents) - coefficients = tuple(coefficients) + exponents = np.array(exponents, dtype=np.float64) + coefficients = np.array(coefficients, dtype=np.float64) angular_momentums = tuple(angular_momentums) magnetic_quantum_numbers = tuple(magnetic_quantum_numbers) @@ -1104,7 +1104,7 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): @pytest.mark.activate_if_skip_heavy def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): """Analytic vs auto-diff gradients/laplacian for three-body Jastrow (AOs).""" - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("jastrow_grad_lap", "strict") num_r_up_cart_samples = 4 num_r_dn_cart_samples = 2 num_R_cart_samples = 5 @@ -1136,8 +1136,8 @@ def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): num_ao=num_ao, num_ao_prim=num_ao_prim, orbital_indices=tuple(orbital_indices), - exponents=tuple(exponents), - coefficients=tuple(coefficients), + exponents=np.array(exponents, dtype=np.float64), + coefficients=np.array(coefficients, dtype=np.float64), angular_momentums=tuple(angular_momentums), magnetic_quantum_numbers=tuple(magnetic_quantum_numbers), ) @@ -1176,7 +1176,7 @@ def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): @pytest.mark.activate_if_skip_heavy def test_analytic_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): """Analytic vs auto-diff gradients/laplacian for three-body Jastrow (MOs).""" - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("jastrow_grad_lap", "strict") num_el = 8 num_mo = 4 num_ao = 3 @@ -1209,8 +1209,8 @@ def test_analytic_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): num_ao=num_ao, num_ao_prim=num_ao_prim, orbital_indices=tuple(orbital_indices), - exponents=tuple(exponents), - coefficients=tuple(coefficients), + exponents=np.array(exponents, dtype=np.float64), + coefficients=np.array(coefficients, dtype=np.float64), angular_momentums=tuple(angular_momentums), magnetic_quantum_numbers=tuple(magnetic_quantum_numbers), ) @@ -1336,7 +1336,7 @@ def _build_jastrow_data_for_part_tests(j1b_type: str = "exp", j2b_type: str = "p @pytest.mark.parametrize("j1b_type,j2b_type,include_nn", _JASTROW_COMBOS) def test_numerical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): """Numerical vs auto-diff gradients/laplacian for J1+J2+J3(+NN).""" - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("jastrow_grad_lap", "loose") jastrow_data, r_up_carts, r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) grad_up_num, grad_dn_num, lap_up_num, lap_dn_num = _compute_grads_and_laplacian_Jastrow_part_debug( @@ -1382,7 +1382,7 @@ def test_numerical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): @pytest.mark.parametrize("j1b_type,j2b_type,include_nn", _JASTROW_COMBOS) def test_analytical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): """Analytic vs auto-diff gradients/laplacian for J1+J2+J3(+NN).""" - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("jastrow_grad_lap", "strict") jastrow_data, r_up_carts, r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) grad_up_an, grad_dn_an, lap_up_an, lap_dn_an = compute_grads_and_laplacian_Jastrow_part( @@ -1418,7 +1418,7 @@ def test_analytical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): @pytest.mark.parametrize("pattern", ["all_moved", "none_moved", "mixed"]) def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern: str): """Compare ratio Jastrow part: debug vs rank-1 update implementation.""" - atol, rtol = get_tolerance("jastrow", "strict") + atol, rtol = get_tolerance("jastrow_eval", "strict") np.random.seed(0) jastrow_data, old_r_up_carts, old_r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) diff --git a/tests/test_jqmc_gfmc_bra.py b/tests/test_jqmc_gfmc_bra.py index c3d6a7fc..071bedd7 100755 --- a/tests/test_jqmc_gfmc_bra.py +++ b/tests/test_jqmc_gfmc_bra.py @@ -181,10 +181,10 @@ def test_jqmc_gfmc_n(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast ) gfmc_jax.run(num_mcmc_steps=num_mcmc_steps) - # e_L / w_L cross orb_eval/jastrow/geminal/coulomb/kinetic/gfmc zones; the + # e_L / w_L cross ao_eval/jastrow_eval/det_eval/coulomb/wf_kinetic zones; the # achievable debug-vs-jax agreement is bounded by the weakest (fp32 in mixed). atol, rtol = get_tolerance_min( - ("orb_eval", "jastrow", "geminal", "determinant", "coulomb", "kinetic", "gfmc"), + ("ao_eval", "jastrow_eval", "det_eval", "coulomb", "wf_kinetic"), "strict", ) diff --git a/tests/test_jqmc_gfmc_tau.py b/tests/test_jqmc_gfmc_tau.py index ba7cacb1..347b63d5 100755 --- a/tests/test_jqmc_gfmc_tau.py +++ b/tests/test_jqmc_gfmc_tau.py @@ -178,10 +178,10 @@ def test_jqmc_gfmc_t(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast ) gfmc_jax.run(num_mcmc_steps=num_mcmc_steps) - # e_L / e_L2 / w_L cross orb_eval/jastrow/geminal/coulomb/kinetic/gfmc zones; + # e_L / e_L2 / w_L cross ao_eval/jastrow_eval/det_eval/coulomb/wf_kinetic zones; # the achievable debug-vs-jax agreement is bounded by the weakest (fp32 in mixed). atol, rtol = get_tolerance_min( - ("orb_eval", "jastrow", "geminal", "determinant", "coulomb", "kinetic", "gfmc"), + ("ao_eval", "jastrow_eval", "det_eval", "coulomb", "wf_kinetic"), "strict", ) diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index bc61c4ec..7121d5c0 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -80,10 +80,10 @@ @pytest.mark.parametrize("trexio_file,with_1b_jastrow,with_2b_jastrow,with_3b_jastrow,with_nn_jastrow", param_grid) def test_jqmc_mcmc(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jastrow, with_nn_jastrow): """Test comparison with MCMC debug and MCMC production implementations.""" - # e_L / w_L cross orb_eval/jastrow/geminal/coulomb/kinetic/mcmc zones; the + # e_L / w_L cross ao_eval/jastrow_eval/det_eval/coulomb/wf_kinetic zones; the # achievable debug-vs-jax agreement is bounded by the weakest (fp32 in mixed). atol, rtol = get_tolerance_min( - ("orb_eval", "jastrow", "geminal", "determinant", "coulomb", "kinetic", "mcmc"), + ("ao_eval", "jastrow_eval", "det_eval", "coulomb", "wf_kinetic"), "strict", ) ( @@ -1034,9 +1034,9 @@ def fake_get_gF( ln_psi_mo = float(evaluate_ln_wavefunction(final_wf, r_up, r_dn)) ln_psi_ao = float(evaluate_ln_wavefunction(wf_ao, r_up, r_dn)) - # ln|Psi| crosses orb_eval/jastrow/geminal/determinant; bound by weakest zone. + # ln|Psi| crosses ao_eval/jastrow_eval/det_eval; bound by weakest zone. atol, rtol = get_tolerance_min( - ("orb_eval", "jastrow", "geminal", "determinant", "mcmc"), + ("ao_eval", "jastrow_eval", "det_eval"), "strict", ) assert not np.any(np.isnan(np.asarray(ln_psi_mo))), "NaN detected in first argument" @@ -1220,9 +1220,9 @@ def fake_get_gF( lam_after = np.asarray(mcmc.hamiltonian_data.wavefunction_data.geminal_data.lambda_matrix) # ── Assertions ─────────────────────────────────────────────────────────── - # j3 / lambda_matrix live in jastrow / geminal zones; symmetry is a structural + # j3 / lambda_matrix live in jastrow_eval / det_eval zones; symmetry is a structural # property of the matrix itself, so use those zones' tolerances. - atol, rtol = get_tolerance_min(("jastrow", "geminal"), "strict") + atol, rtol = get_tolerance_min(("jastrow_eval", "det_eval"), "strict") if j3_type == "sym": np.testing.assert_allclose( j3_after[:, :-1], @@ -1611,7 +1611,7 @@ def test_get_aH_and_solve_lm_debug_vs_production(): # H_0/f/S/K/B cross the full e_L path + optimization assembly; bound by weakest zone. atol, rtol = get_tolerance_min( - ("orb_eval", "jastrow", "geminal", "determinant", "coulomb", "kinetic", "mcmc", "optimization"), + ("ao_eval", "jastrow_eval", "det_eval", "coulomb", "local_energy"), "strict", ) diff --git a/tests/test_jqmc_tool.py b/tests/test_jqmc_tool.py index 2f8cfcda..f6ed9c8a 100644 --- a/tests/test_jqmc_tool.py +++ b/tests/test_jqmc_tool.py @@ -604,7 +604,7 @@ def test_mcmc_random_energy_jackknife(self, tmp_path): m = re.search(r"E\s*=\s*([+-]?[\d.eE+-]+)\s*\+-\s*([\d.eE+-]+)", result.output) assert m is not None E_cli, std_cli = float(m.group(1)), float(m.group(2)) - atol, rtol = get_tolerance("io", "strict") + atol, rtol = get_tolerance("local_energy", "strict") np.testing.assert_allclose(E_cli, E_ref, atol=atol, rtol=rtol) np.testing.assert_allclose(std_cli, std_ref, atol=atol, rtol=rtol) @@ -636,7 +636,7 @@ def test_mcmc_multi_rank_random(self, tmp_path): m = re.search(r"E\s*=\s*([+-]?[\d.eE+-]+)\s*\+-\s*([\d.eE+-]+)", result.output) assert m is not None E_cli, std_cli = float(m.group(1)), float(m.group(2)) - atol, rtol = get_tolerance("io", "strict") + atol, rtol = get_tolerance("local_energy", "strict") np.testing.assert_allclose(E_cli, E_ref, atol=atol, rtol=rtol) np.testing.assert_allclose(std_cli, std_ref, atol=atol, rtol=rtol) @@ -668,7 +668,7 @@ def test_mcmc_warmup_discards_steps(self, tmp_path): m = re.search(r"E\s*=\s*([+-]?[\d.eE+-]+)\s*\+-\s*([\d.eE+-]+)", result.output) assert m is not None E_cli, std_cli = float(m.group(1)), float(m.group(2)) - atol, rtol = get_tolerance("io", "strict") + atol, rtol = get_tolerance("local_energy", "strict") np.testing.assert_allclose(E_cli, E_ref, atol=atol, rtol=rtol) np.testing.assert_allclose(std_cli, std_ref, atol=atol, rtol=rtol) @@ -699,7 +699,7 @@ def test_lrdmc_random_energy_jackknife(self, tmp_path): m = re.search(r"E\s*=\s*([+-]?[\d.eE+-]+)\s*\+-\s*([\d.eE+-]+)", result.output) assert m is not None E_cli, std_cli = float(m.group(1)), float(m.group(2)) - atol, rtol = get_tolerance("io", "strict") + atol, rtol = get_tolerance("local_energy", "strict") np.testing.assert_allclose(E_cli, E_ref, atol=atol, rtol=rtol) np.testing.assert_allclose(std_cli, std_ref, atol=atol, rtol=rtol) @@ -731,7 +731,7 @@ def test_lrdmc_multi_rank_random(self, tmp_path): m = re.search(r"E\s*=\s*([+-]?[\d.eE+-]+)\s*\+-\s*([\d.eE+-]+)", result.output) assert m is not None E_cli, std_cli = float(m.group(1)), float(m.group(2)) - atol, rtol = get_tolerance("io", "strict") + atol, rtol = get_tolerance("local_energy", "strict") np.testing.assert_allclose(E_cli, E_ref, atol=atol, rtol=rtol) np.testing.assert_allclose(std_cli, std_ref, atol=atol, rtol=rtol) diff --git a/tests/test_lrdmc_force.py b/tests/test_lrdmc_force.py index 3ee6e7d4..337d8ef1 100755 --- a/tests/test_lrdmc_force.py +++ b/tests/test_lrdmc_force.py @@ -44,7 +44,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance # noqa: E402 +from jqmc._precision import get_tolerance_min # noqa: E402 from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -203,8 +203,12 @@ def test_lrdmc_force_with_SWCT_n(trexio_file: str, jastrow_parameters: dict, loc num_mcmc_bin_blocks=num_mcmc_bin_blocks, ) + # Force crosses ao_eval/jastrow_eval/det_eval/coulomb/wf_kinetic; bound by weakest zone (fp32 in mixed). # See [J. Chem. Phys. 156, 034101 (2022)] - atol, rtol = get_tolerance("gfmc", "strict") + atol, rtol = get_tolerance_min( + ("ao_eval", "jastrow_eval", "det_eval", "coulomb", "wf_kinetic"), + "strict", + ) assert not np.any(np.isnan(np.asarray(np.array(force_mean[0])))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(-1.0 * np.array(force_mean[1])))), "NaN detected in second argument" np.testing.assert_allclose( @@ -315,8 +319,12 @@ def test_lrdmc_force_with_SWCT_t(trexio_file: str, jastrow_parameters: dict, loc num_mcmc_bin_blocks=num_mcmc_bin_blocks, ) + # Force crosses ao_eval/jastrow_eval/det_eval/coulomb/wf_kinetic; bound by weakest zone (fp32 in mixed). # See [J. Chem. Phys. 156, 034101 (2022)] - atol, rtol = get_tolerance("gfmc", "strict") + atol, rtol = get_tolerance_min( + ("ao_eval", "jastrow_eval", "det_eval", "coulomb", "wf_kinetic"), + "strict", + ) assert not np.any(np.isnan(np.asarray(np.array(force_mean[0])))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(-1.0 * np.array(force_mean[1])))), "NaN detected in second argument" np.testing.assert_allclose( diff --git a/tests/test_mcmc_force.py b/tests/test_mcmc_force.py index 8bba9bee..e53a4985 100755 --- a/tests/test_mcmc_force.py +++ b/tests/test_mcmc_force.py @@ -44,7 +44,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance # noqa: E402 +from jqmc._precision import get_tolerance_min # noqa: E402 from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -198,8 +198,12 @@ def test_mcmc_force_with_SWCT(trexio_file: str, jastrow_parameters: dict): num_mcmc_bin_blocks=num_mcmc_bin_blocks, ) + # Force crosses ao_eval/jastrow_eval/det_eval/coulomb/wf_kinetic; bound by weakest zone (fp32 in mixed). # See [J. Chem. Phys. 156, 034101 (2022)] - atol, rtol = get_tolerance("mcmc", "strict") + atol, rtol = get_tolerance_min( + ("ao_eval", "jastrow_eval", "det_eval", "coulomb", "wf_kinetic"), + "strict", + ) assert not np.any(np.isnan(np.asarray(np.array(force_mean[0])))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(-1.0 * np.array(force_mean[1])))), "NaN detected in second argument" np.testing.assert_allclose( diff --git a/tests/test_mixed_precision.py b/tests/test_mixed_precision.py index 0824a834..95fa694d 100644 --- a/tests/test_mixed_precision.py +++ b/tests/test_mixed_precision.py @@ -31,7 +31,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import configure, get_dtype # noqa: E402 +from jqmc._precision import configure, get_dtype_jnp # noqa: E402 from jqmc.atomic_orbital import ( # noqa: E402 compute_AOs, compute_AOs_grad, @@ -54,12 +54,12 @@ from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, Jastrow_one_body_data, - Jastrow_two_body_data, Jastrow_three_body_data, + Jastrow_two_body_data, compute_Jastrow_one_body, compute_Jastrow_part, - compute_Jastrow_two_body, compute_Jastrow_three_body, + compute_Jastrow_two_body, ) from jqmc.molecular_orbital import ( # noqa: E402 compute_MOs, @@ -178,25 +178,25 @@ class TestAODtype: """Verify AO evaluation outputs are float32 in mixed mode.""" def test_compute_AOs_output_dtype(self, h2_data): - """compute_AOs must return float32 (orb_eval zone).""" + """compute_AOs must return float32 (ao_eval zone).""" AOs = compute_AOs(h2_data["aos_data"], h2_data["r_up"]) - expected = get_dtype("orb_eval") + expected = get_dtype_jnp("ao_eval") assert AOs.dtype == expected, ( f"compute_AOs output dtype is {AOs.dtype}, expected {expected}. " - "Likely cause: fp64 data (R_carts, exponents, coefficients) not cast to orb_eval dtype inside kernel." + "Likely cause: fp64 data (R_carts, exponents, coefficients) not cast to ao_eval dtype inside kernel." ) def test_compute_AOs_grad_output_dtype(self, h2_data): - """compute_AOs_grad must return float in kinetic zone dtype.""" + """compute_AOs_grad must return ao_grad_lap zone dtype.""" grad_x, grad_y, grad_z = compute_AOs_grad(h2_data["aos_data"], h2_data["r_up"]) - expected = get_dtype("kinetic") + expected = get_dtype_jnp("ao_grad_lap") for name, arr in [("grad_x", grad_x), ("grad_y", grad_y), ("grad_z", grad_z)]: assert arr.dtype == expected, f"compute_AOs_grad {name} dtype is {arr.dtype}, expected {expected}." def test_compute_AOs_laplacian_output_dtype(self, h2_data): - """compute_AOs_laplacian must return kinetic zone dtype.""" + """compute_AOs_laplacian must return ao_grad_lap zone dtype.""" lap = compute_AOs_laplacian(h2_data["aos_data"], h2_data["r_up"]) - expected = get_dtype("kinetic") + expected = get_dtype_jnp("ao_grad_lap") assert lap.dtype == expected, f"compute_AOs_laplacian dtype is {lap.dtype}, expected {expected}." @@ -216,12 +216,12 @@ class TestMODtype: """ def test_compute_MOs_output_dtype(self, h2_data): - """compute_MOs must return determinant-zone dtype (fp64 in mixed).""" + """compute_MOs must return mo_eval-zone dtype (fp64 in mixed).""" MOs = compute_MOs(h2_data["mos_data_up"], h2_data["r_up"]) - expected = get_dtype("determinant") + expected = get_dtype_jnp("mo_eval") assert MOs.dtype == expected, ( f"compute_MOs output dtype is {MOs.dtype}, expected {expected}. " - "compute_MOs should upcast its small matmul to the determinant zone " + "compute_MOs should upcast its small matmul to the mo_eval zone " "to avoid fp32 amplification downstream." ) @@ -241,7 +241,7 @@ def test_jastrow_two_body_output_dtype(self, h2_data): """ j2_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="pade") J2 = compute_Jastrow_two_body(j2_data, h2_data["r_up"], h2_data["r_dn"]) - expected = get_dtype("jastrow") + expected = get_dtype_jnp("jastrow_eval") assert jnp.asarray(J2).dtype == expected, ( f"compute_Jastrow_two_body dtype is {jnp.asarray(J2).dtype}, expected {expected}. " "Likely cause: jastrow_2b_param not cast to jastrow dtype." @@ -256,7 +256,7 @@ def test_jastrow_three_body_output_dtype(self, h2_data): orb_data=h2_data["aos_data"], random_init=True, random_scale=1e-3 ) J3 = compute_Jastrow_three_body(j3_data, h2_data["r_up"], h2_data["r_dn"]) - expected = get_dtype("jastrow") + expected = get_dtype_jnp("jastrow_eval") assert jnp.asarray(J3).dtype == expected, ( f"compute_Jastrow_three_body dtype is {jnp.asarray(J3).dtype}, expected {expected}. " "Likely cause: j_matrix not cast to jastrow dtype." @@ -274,7 +274,7 @@ def test_jastrow_part_output_dtype(self, h2_data): jastrow_three_body_data=j3_data, ) J = compute_Jastrow_part(jastrow_data, h2_data["r_up"], h2_data["r_dn"]) - expected = get_dtype("jastrow") + expected = get_dtype_jnp("jastrow_eval") assert jnp.asarray(J).dtype == expected, f"compute_Jastrow_part dtype is {jnp.asarray(J).dtype}, expected {expected}." @@ -292,7 +292,7 @@ def test_geminal_matrix_output_dtype(self, h2_data): Catches: lambda_matrix or AO data not cast to geminal dtype. """ G = compute_geminal_all_elements(h2_data["geminal_data"], h2_data["r_up"], h2_data["r_dn"]) - expected = get_dtype("geminal") + expected = get_dtype_jnp("det_eval") assert G.dtype == expected, f"compute_geminal_all_elements dtype is {G.dtype}, expected {expected}." @@ -310,7 +310,7 @@ def test_ln_det_output_dtype(self, h2_data): Even though geminal matrix is float32, the log-det must be computed in float64. """ ln_det = compute_ln_det_geminal_all_elements(h2_data["geminal_data"], h2_data["r_up"], h2_data["r_dn"]) - expected = get_dtype("determinant") + expected = get_dtype_jnp("det_eval") assert jnp.asarray(ln_det).dtype == expected, ( f"compute_ln_det dtype is {jnp.asarray(ln_det).dtype}, expected {expected}." ) @@ -327,7 +327,7 @@ class TestCoulombDtype: def test_bare_coulomb_el_el_output_dtype(self, h2_data): """Electron-electron Coulomb must return float32 (coulomb zone).""" V = compute_bare_coulomb_potential_el_el(h2_data["r_up"], h2_data["r_dn"]) - expected = get_dtype("coulomb") + expected = get_dtype_jnp("coulomb") assert jnp.asarray(V).dtype == expected, ( f"compute_bare_coulomb_potential_el_el dtype is {jnp.asarray(V).dtype}, expected {expected}." ) @@ -340,7 +340,7 @@ def test_bare_coulomb_el_ion_output_dtype(self, h2_data): V_up, V_dn = compute_bare_coulomb_potential_el_ion_element_wise( h2_data["coulomb_data"], h2_data["r_up"], h2_data["r_dn"] ) - expected = get_dtype("coulomb") + expected = get_dtype_jnp("coulomb") assert V_up.dtype == expected, ( f"el_ion V_up dtype is {V_up.dtype}, expected {expected}. " "Likely cause: R_charges or R_carts not cast to coulomb dtype." @@ -349,7 +349,7 @@ def test_bare_coulomb_el_ion_output_dtype(self, h2_data): def test_bare_coulomb_total_output_dtype(self, h2_data): """Total bare Coulomb must return float32 (coulomb zone).""" V = compute_bare_coulomb_potential(h2_data["coulomb_data"], h2_data["r_up"], h2_data["r_dn"]) - expected = get_dtype("coulomb") + expected = get_dtype_jnp("coulomb") assert jnp.asarray(V).dtype == expected, ( f"compute_bare_coulomb_potential dtype is {jnp.asarray(V).dtype}, expected {expected}." ) @@ -364,10 +364,10 @@ class TestKineticDtype: """Verify kinetic energy stays float64 in mixed mode.""" def test_kinetic_energy_output_dtype(self, h2_data): - """compute_kinetic_energy must return float64 (kinetic zone).""" + """compute_kinetic_energy must return float64 (wf_kinetic zone).""" wf_data = Wavefunction_data(geminal_data=h2_data["geminal_data"]) T = compute_kinetic_energy(wf_data, h2_data["r_up"], h2_data["r_dn"]) - expected = get_dtype("kinetic") + expected = get_dtype_jnp("wf_kinetic") assert jnp.asarray(T).dtype == expected, f"compute_kinetic_energy dtype is {jnp.asarray(T).dtype}, expected {expected}." @@ -393,7 +393,7 @@ def test_ln_wavefunction_output_dtype(self, h2_data): ) wf_data = Wavefunction_data(geminal_data=h2_data["geminal_data"], jastrow_data=jastrow_data) ln_psi = evaluate_ln_wavefunction(wf_data, h2_data["r_up"], h2_data["r_dn"]) - expected = get_dtype("determinant") + expected = get_dtype_jnp("wf_eval") assert jnp.asarray(ln_psi).dtype == expected, ( f"evaluate_ln_wavefunction dtype is {jnp.asarray(ln_psi).dtype}, expected {expected}." ) @@ -409,16 +409,16 @@ class TestAOSpheDtype: def test_compute_AOs_sphe_output_dtype(self, h2_sphe_data): AOs = compute_AOs(h2_sphe_data["aos_data"], h2_sphe_data["r_up"]) - _assert_dtype(AOs, get_dtype("orb_eval"), "compute_AOs (sphe)") + _assert_dtype(AOs, get_dtype_jnp("ao_eval"), "compute_AOs (sphe)") def test_compute_AOs_sphe_grad_output_dtype(self, h2_sphe_data): gx, gy, gz = compute_AOs_grad(h2_sphe_data["aos_data"], h2_sphe_data["r_up"]) for name, arr in [("grad_x", gx), ("grad_y", gy), ("grad_z", gz)]: - _assert_dtype(arr, get_dtype("kinetic"), f"compute_AOs_grad sphe {name}") + _assert_dtype(arr, get_dtype_jnp("ao_grad_lap"), f"compute_AOs_grad sphe {name}") def test_compute_AOs_sphe_laplacian_output_dtype(self, h2_sphe_data): lap = compute_AOs_laplacian(h2_sphe_data["aos_data"], h2_sphe_data["r_up"]) - _assert_dtype(lap, get_dtype("kinetic"), "compute_AOs_laplacian (sphe)") + _assert_dtype(lap, get_dtype_jnp("ao_grad_lap"), "compute_AOs_laplacian (sphe)") class TestMOExtendedDtype: @@ -426,13 +426,13 @@ class TestMOExtendedDtype: def test_compute_MOs_grad_output_dtype(self, h2_data): gx, gy, gz = compute_MOs_grad(h2_data["mos_data_up"], h2_data["r_up"]) - expected = get_dtype("kinetic") + expected = get_dtype_jnp("mo_grad_lap") for name, arr in [("grad_x", gx), ("grad_y", gy), ("grad_z", gz)]: _assert_dtype(arr, expected, f"compute_MOs_grad {name}") def test_compute_MOs_laplacian_output_dtype(self, h2_data): lap = compute_MOs_laplacian(h2_data["mos_data_up"], h2_data["r_up"]) - _assert_dtype(lap, get_dtype("kinetic"), "compute_MOs_laplacian") + _assert_dtype(lap, get_dtype_jnp("mo_grad_lap"), "compute_MOs_laplacian") class TestJastrowOneBodyDtype: @@ -447,7 +447,7 @@ def test_compute_Jastrow_one_body_output_dtype(self, h2_data): jastrow_1b_type="pade", ) J1 = compute_Jastrow_one_body(j1_data, h2_data["r_up"], h2_data["r_dn"]) - _assert_dtype(J1, get_dtype("jastrow"), "compute_Jastrow_one_body") + _assert_dtype(J1, get_dtype_jnp("jastrow_eval"), "compute_Jastrow_one_body") class TestGeminalFastUpdateDtype: @@ -461,12 +461,12 @@ def water_data(self): def test_geminal_up_one_row_output_dtype(self, water_data): # Use [0:1] to get shape (1, 3) — compute_orb_api requires (N, 3), not (3,) row = compute_geminal_up_one_row_elements(water_data["geminal_data"], water_data["r_up"][0:1], water_data["r_dn"]) - _assert_dtype(row, get_dtype("geminal"), "compute_geminal_up_one_row_elements") + _assert_dtype(row, get_dtype_jnp("det_ratio"), "compute_geminal_up_one_row_elements") def test_geminal_dn_one_column_output_dtype(self, water_data): # Use [0:1] to get shape (1, 3) — compute_orb_api requires (N, 3), not (3,) col = compute_geminal_dn_one_column_elements(water_data["geminal_data"], water_data["r_up"], water_data["r_dn"][0:1]) - _assert_dtype(col, get_dtype("geminal"), "compute_geminal_dn_one_column_elements") + _assert_dtype(col, get_dtype_jnp("det_ratio"), "compute_geminal_dn_one_column_elements") class TestECPDtype: @@ -477,7 +477,7 @@ class TestECPDtype: def test_compute_ecp_local_parts_output_dtype(self, h2_ecp_data): V_loc = compute_ecp_local_parts_all_pairs(h2_ecp_data["coulomb_data"], h2_ecp_data["r_up"], h2_ecp_data["r_dn"]) - _assert_dtype(V_loc, get_dtype("coulomb"), "compute_ecp_local_parts_all_pairs") + _assert_dtype(V_loc, get_dtype_jnp("coulomb"), "compute_ecp_local_parts_all_pairs") def test_compute_ecp_non_local_eval_shape_dtype(self, h2_ecp_data): """Heavy ECP non-local kernel: verify dtype via jax.eval_shape (no execution).""" @@ -498,7 +498,7 @@ def test_compute_ecp_non_local_eval_shape_dtype(self, h2_ecp_data): ) _assert_eval_shape_dtype( compute_ecp_non_local_part_all_pairs_jax_weights_grid_points, - get_dtype("coulomb"), + get_dtype_jnp("coulomb"), "compute_ecp_non_local_part_all_pairs_jax_weights_grid_points", h2_ecp_data["coulomb_data"], wf_data, @@ -521,7 +521,7 @@ def test_compute_local_energy_output_dtype(self, h2_data): ) RT = jnp.eye(3, dtype=jnp.float64) e_L = compute_local_energy(ham, h2_data["r_up"], h2_data["r_dn"], RT) - _assert_dtype(e_L, get_dtype("kinetic"), "compute_local_energy") + _assert_dtype(e_L, get_dtype_jnp("local_energy"), "compute_local_energy") class TestKineticEvalShape: @@ -535,7 +535,7 @@ def test_kinetic_energy_eval_shape_dtype(self, h2_data): wf_data = Wavefunction_data(geminal_data=h2_data["geminal_data"]) _assert_eval_shape_dtype( compute_kinetic_energy, - get_dtype("kinetic"), + get_dtype_jnp("wf_kinetic"), "compute_kinetic_energy (eval_shape)", wf_data, h2_data["r_up"], @@ -546,7 +546,7 @@ def test_evaluate_ln_wavefunction_eval_shape_dtype(self, h2_data): wf_data = Wavefunction_data(geminal_data=h2_data["geminal_data"]) _assert_eval_shape_dtype( evaluate_ln_wavefunction, - get_dtype("determinant"), + get_dtype_jnp("wf_eval"), "evaluate_ln_wavefunction (eval_shape)", wf_data, h2_data["r_up"], diff --git a/tests/test_structure.py b/tests/test_structure.py index 7a5ced36..4330bc9c 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -54,7 +54,7 @@ def _make_non_pbc_structure(): def test_reciprocal_lattice_dot_2pi(): """Test that the dot product of the cell and reciprocal cell gives 2pi delta_ij.""" - atol, rtol = get_tolerance("io", "strict") + atol, rtol = get_tolerance("local_energy", "strict") structure = _make_pbc_structure() recip = structure.recip_cell cell = structure.cell @@ -69,7 +69,7 @@ def test_reciprocal_lattice_dot_2pi(): def test_np_jnp_consistency_non_pbc(): """Test consistency between NumPy and JAX implementations for non-PBC structures.""" - atol, rtol = get_tolerance("io", "strict") + atol, rtol = get_tolerance("local_energy", "strict") structure = _make_non_pbc_structure() r_cart = np.array([0.2, 0.0, 0.0]) @@ -100,7 +100,7 @@ def test_np_jnp_consistency_non_pbc(): def test_pbc_minimum_image_and_nearest(): """Test PBC minimum image convention and nearest nucleus finding.""" - atol, rtol = get_tolerance("io", "strict") + atol, rtol = get_tolerance("local_energy", "strict") structure = _make_pbc_structure() r_cart = np.array([9.1, 0.0, 0.0]) @@ -145,7 +145,7 @@ def test_pbc_minimum_image_and_nearest(): @pytest.mark.parametrize("use_pbc", [False, True]) def test_find_nearest_index_matches_min_dist_jnp(use_pbc): """Test that the nearest index found matches the minimum distance calculation.""" - atol, rtol = get_tolerance("io", "strict") + atol, rtol = get_tolerance("local_energy", "strict") structure = _make_pbc_structure() if use_pbc else _make_non_pbc_structure() r_cart = np.array([9.1, 0.0, 0.0]) if use_pbc else np.array([1.8, 0.1, 0.0]) diff --git a/tests/test_swct.py b/tests/test_swct.py index 6bd1b397..17b4d693 100755 --- a/tests/test_swct.py +++ b/tests/test_swct.py @@ -61,7 +61,7 @@ @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5"]) def test_debug_and_jax_SWCT_omega(trexio_file: str): """Test SWCT omega, compare debug and jax.""" - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("swct", "strict") ( structure_data, _, diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index c81d7874..64cb2b6c 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -121,7 +121,7 @@ def test_kinetic_energy_analytic_and_numerical(trexio_file: str): K_debug = _compute_kinetic_energy_debug(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) K_jax = compute_kinetic_energy(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("wf_kinetic", "loose") assert not np.any(np.isnan(np.asarray(np.asarray(K_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(K_jax)))), "NaN detected in second argument" np.testing.assert_allclose( @@ -172,7 +172,7 @@ def test_kinetic_energy_analytic_and_auto(trexio_file: str): r_dn_carts=jnp.asarray(r_dn_carts), ) - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("wf_kinetic", "strict") assert not np.any(np.isnan(np.asarray(K_analytic))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_auto))), "NaN detected in second argument" np.testing.assert_allclose(K_analytic, K_auto, atol=atol, rtol=rtol) @@ -221,7 +221,7 @@ def test_debug_and_auto_kinetic_energy_all_elements(trexio_file: str): wavefunction_data=wavefunction_data, r_up_carts=r_up_carts_jnp, r_dn_carts=r_dn_carts_jnp ) - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("wf_kinetic", "loose") assert not np.any(np.isnan(np.asarray(K_elements_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_up_auto))), "NaN detected in second argument" np.testing.assert_allclose(K_elements_up_debug, K_elements_up_auto, atol=atol, rtol=rtol) @@ -291,7 +291,7 @@ def test_auto_and_analytic_kinetic_energy_all_elements(trexio_file: str): wavefunction_data=wavefunction_data, r_up_carts=r_up_carts_jnp, r_dn_carts=r_dn_carts_jnp ) - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("wf_kinetic", "strict") assert not np.any(np.isnan(np.asarray(K_elements_up_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_up_analytic))), "NaN detected in second argument" np.testing.assert_allclose(K_elements_up_auto, K_elements_up_analytic, atol=atol, rtol=rtol) @@ -359,7 +359,7 @@ def test_fast_update_kinetic_energy_all_elements(trexio_file: str): geminal_inverse=A_inv, ) - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("wf_kinetic", "strict") assert not np.any(np.isnan(np.asarray(ke_up_fast))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(ke_up_debug))), "NaN detected in second argument" np.testing.assert_allclose(ke_up_fast, ke_up_debug, atol=atol, rtol=rtol) @@ -441,7 +441,7 @@ def test_debug_and_jax_discretized_kinetic_energy(trexio_file: str): RT=RT, ) - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("wf_kinetic", "strict") assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_up_carts_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_up_carts_debug))), "NaN detected in second argument" np.testing.assert_allclose( @@ -542,7 +542,7 @@ def test_nodal_distance_analytic_vs_debug(trexio_file: str): ) # They should be identical up to numerical noise - atol, rtol = get_tolerance("kinetic", "loose") + atol, rtol = get_tolerance("wf_kinetic", "loose") np.testing.assert_allclose( np.asarray(nd_analytic), np.asarray(nd_debug), @@ -595,7 +595,7 @@ def test_evaluate_ln_wavefunction_fast_forward(trexio_file): n_up = geminal_data.num_electron_up n_dn = geminal_data.num_electron_dn - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("wf_kinetic", "strict") for _ in range(10): r_up = jnp.array(rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) @@ -632,7 +632,7 @@ def test_evaluate_ln_wavefunction_fast_backward(trexio_file): grad_ref_fn = jax.grad(evaluate_ln_wavefunction, argnums=0) grad_fast_fn = jax.grad(evaluate_ln_wavefunction_fast, argnums=0) - atol, rtol = get_tolerance("kinetic", "strict") + atol, rtol = get_tolerance("wf_kinetic", "strict") for _ in range(10): r_up = jnp.array(rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) From eff4fd5f844417fdae057befb7d3d566386714a3 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:59:06 +0900 Subject: [PATCH 09/97] Update doc. --- doc/notes/mixed_precision.md | 159 +++++++++++++++++++++++++++++------ 1 file changed, 135 insertions(+), 24 deletions(-) diff --git a/doc/notes/mixed_precision.md b/doc/notes/mixed_precision.md index f10aee73..33d28936 100644 --- a/doc/notes/mixed_precision.md +++ b/doc/notes/mixed_precision.md @@ -30,21 +30,44 @@ Or keep the default (all float64, backward compatible): ## Precision zones -jQMC divides the computation into 10 **Precision Zones**. The mapping -from zone to dtype is determined entirely by the chosen mode: - -| Zone | Components | `full` | `mixed` | float32 risk | -|----------------|-----------------------------------|--------|---------|--------------| -| `orb_eval` | AO/MO forward evaluation | f64 | **f32** | low | -| `jastrow` | Jastrow factor (J1/J2/J3) | f64 | **f32** | low | -| `geminal` | Geminal matrix elements | f64 | f64 | high | -| `determinant` | log-det, SVD, AS regularization | f64 | f64 | high | -| `coulomb` | Coulomb + ECP potential | f64 | **f32** | low-medium | -| `kinetic` | Kinetic energy + AO/MO derivatives | f64 | f64 | high | -| `mcmc` | MCMC sampling | f64 | f64 | high | -| `gfmc` | GFMC propagation | f64 | f64 | high | -| `optimization` | SR matrix, parameter updates | f64 | f64 | high | -| `io` | I/O, structure data | f64 | f64 | low-medium | +jQMC divides the computation into 16 **Precision Zones**. Each zone is +owned by exactly one module and is named for its *purpose* (not its +dtype). The mapping from zone to dtype is determined entirely by the +chosen mode. + +| Zone | Owning module | `full` | `mixed` | risk | E_L path | +|--------------------|------------------------|--------|----------|----------|------------| +| `ao_eval` | `atomic_orbital` | f64 | **f32** | low | core | +| `ao_grad_lap` | `atomic_orbital` | f64 | **f32** | low | core | +| `mo_eval` | `molecular_orbital` | f64 | f64 | high\* | core | +| `mo_grad_lap` | `molecular_orbital` | f64 | f64 | high | core | +| `jastrow_eval` | `jastrow_factor` | f64 | **f32** | low | core† | +| `jastrow_grad_lap` | `jastrow_factor` | f64 | **f32** | low | core | +| `jastrow_ratio` | `jastrow_factor` | f64 | **f32** | low | indirect‡ | +| `det_eval` | `determinant` | f64 | f64 | high | core | +| `det_grad_lap` | `determinant` | f64 | f64 | high | core | +| `det_ratio` | `determinant` | f64 | f64 | high | indirect‡ | +| `coulomb` | `coulomb_potential` | f64 | **f32** | low-med | core | +| `wf_eval` | `wavefunction` | f64 | f64 | high | core† | +| `wf_kinetic` | `wavefunction` | f64 | f64 | high | core | +| `wf_ratio` | `wavefunction` | f64 | f64 | high | no | +| `local_energy` | `hamiltonians` | f64 | f64 | high | core | +| `swct` | `swct` | f64 | f64 | high | no | + +\* `mo_eval` is high-risk even though the consumed AO values are fp32: +the small `mo_coefficients @ aos` matmul runs in this zone, and its +output feeds the determinant matrix where fp32 round-off is amplified +by `log|det|`. + +† `jastrow_eval` and `wf_eval` are on the E_L core path but their +forward values (J and ln|Psi|) do not enter the E_L formula directly +(E_L depends on *derivatives* of ln|Psi|). Diagnostics show zero E_L +bias when these zones alone are fp32. + +‡ `det_ratio` and `jastrow_ratio` affect E_L **indirectly** through the +ECP non-local potential, which evaluates `Psi(R')/Psi(R)` on a +quadrature grid via rank-1 ratio updates. In non-ECP systems these +zones have no E_L impact. ## Workflow integration @@ -67,17 +90,105 @@ those dicts directly or use `_set_zone()` after calling `configure()`. ## Design principles -1. **Explicit dtype declaration** — Every function declares its Precision Zone - and specifies dtype for all arrays. No reliance on JAX implicit promotion. +The implementation rests on **three** principles documented at the top of +`jqmc/_precision.py`. Principle 3 is the most important in practice; almost +every precision bug we have seen is a violation of 3a or 3b. + +**Principle 1 — One Precision Zone is owned by exactly one module.** +A zone (e.g. `ao_eval`, `coulomb`) is *defined and consumed* in a single +module. The mapping zone ↔ owning module is one-to-one. + +**Principle 2 — A module may own multiple Precision Zones.** +Different code paths in the same module legitimately need different +precisions (e.g. `ao_eval` vs `ao_grad_lap`, or `det_eval` vs `det_ratio`). +Each zone is named for its *purpose*, not for its dtype. + +**Principle 3 — Cast responsibility lies with the function that does +arithmetic on the value, never with passthrough wrappers.** + +* **3a (frozen args).** Function arguments are *frozen*: the parameter name + must not be rebound for the entire body of the function. Writing + `arg = jnp.asarray(arg, dtype=...)` at the top of a function is forbidden + — it silently coerces the argument for every later use, including + forwarding to other functions. When the function consumes `arg` as an + arithmetic operand, the cast appears **inside the expression** + (`arg.astype(dtype)`), or — if the cast result is reused — through a + *new* local variable (e.g. `arg_local = arg.astype(dtype)`). The + original `arg` always remains frozen. + +* **3b (local cast at the point of arithmetic).** A function casts a value + to its own zone's dtype **immediately before** consuming it as an + operand. Inputs and outputs of the function's arithmetic both live in + its zone. For catastrophic cancellation (`r - R`): reconstruct the + difference in the dtype the values were received in (the + caller-supplied precision — fp64 in jQMC because the upstream MCMC + walker state is fp64), then down-cast the result to the function's own + zone. The principle is "use the caller-supplied precision," **not** + "hardcode fp64." -2. **Zone boundaries** — When results cross zone boundaries (e.g. Jastrow - float32 → determinant float64), explicit casts ensure the higher-precision - zone receives correctly typed inputs. +```python +# WRONG (3a violation): rebinding `r_carts` silently forwards a +# fp32-truncated array to compute_AOs even though `ao_eval` is fp64. +def compute_coulomb(r_carts, R_carts): + dtype_jnp = get_dtype_jnp("coulomb") + r_carts = jnp.asarray(r_carts, dtype=dtype_jnp) # <-- forbidden + ao = compute_AOs(..., r_carts, R_carts) # downstream sees fp32 + diff = r_carts - R_carts + ... + +# RIGHT: forwarding stays in caller's dtype; reconstruction is in +# caller-supplied precision; downcast happens at the use site. +def compute_coulomb(r_carts, R_carts): + ao = compute_AOs(..., r_carts, R_carts) # forward as-is + dtype_jnp = get_dtype_jnp("coulomb") + diff = (r_carts - R_carts).astype(dtype_jnp) # 3b + ... +``` -3. **Backward compatibility** — Default mode is `"full"` (all float64). - Existing input files work without modification. +### No hardcoded dtype literals + +Inside any module that owns a selectable-precision zone, **never hardcode** +`jnp.float64` / `np.float64` / `jnp.float32` / `np.float32` for arrays the +module produces or consumes. Always go through `get_dtype_jnp("")` +/ `get_dtype_np("")` so the dtype follows the active mode +automatically. + +The exemptions (modules whose data is *always fp64 by construction*, +independent of mode) are: + +* `mcmc` / `gfmc` — MCMC and GFMC walker state. +* I/O modules — `structure`, `trexio_wrapper`, `_jqmc_utility`, + `jqmc_tool`, and the `_load_dataclass_from_hdf5` / + `_save_dataclass_to_hdf5` helpers in `hamiltonians`. On-disk numerical + data (AO exponents/coefficients, nuclear coordinates, geminal + coefficients, etc.) is always fp64 because fp32 storage would silently + lose precision that no downstream upcast can recover. +* **Basis-data storage accessors.** `_*_jnp` properties on + selectable-precision dataclasses whose underlying storage field is + typed `npt.NDArray[np.float64]` are *lift-only* adapters + (numpy → `jax.Array`), not arithmetic. The dtype is fp64 by + construction (storage is loaded from HDF5/TREXIO/optimizer output); + the consumer is responsible for casting the lifted array to its own + zone at the use site (Principle 3b). Concretely this covers + `_exponents_jnp` / `_coefficients_jnp` / + `_normalization_factorial_ratio_prim_jnp` in `atomic_orbital`, + `_mo_coefficients_jnp` in `molecular_orbital`, + `_lambda_matrix_jnp` in `determinant`, `_j_matrix_jnp` in + `jastrow_factor`, and the `ShellPrimMap.from_aos_data` constructor in + `atomic_orbital`. ## API reference -See {py:mod}`jqmc._precision` for the programmatic API (`get_dtype`, -`configure`, `get_tolerance`, etc.). +See {py:mod}`jqmc._precision` for the programmatic API: + +* `get_dtype_jnp(zone)` / `get_dtype_np(zone)` — return the JAX / NumPy + dtype currently assigned to *zone*. +* `get_eps(name, dtype)` — return a dtype-aware numerical-stability + constant (e.g. `"rcond_svd"`, `"stabilizing_ao"`). +* `configure(mode)` — programmatically switch the active precision mode. +* `get_tolerance(zone, level)` — return `(atol, rtol)` for tests, scaled + by the zone's current dtype (`level` = `"strict"` or `"loose"`). +* `get_tolerance_min(zones, level)` — return the loosest `(atol, rtol)` + across the given zones. Use this when a test compares two paths whose + combined dtype span crosses multiple zones; the achievable agreement + is bounded by the weakest zone on the path. From d9ae01aeb8ccd6291ab510363061f817635650a1 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:37:56 +0900 Subject: [PATCH 10/97] Update github actions. --- .github/workflows/jqmc-run-full-pytest.yml | 11 ++----- .github/workflows/jqmc-run-rc-pytest.yml | 32 ++++++++++++++++++--- .github/workflows/jqmc-run-short-pytest.yml | 12 ++++++++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index 0f10d85d..2d7574a8 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -3,7 +3,7 @@ name: jqmc full test on: - push: + pull_request: branches: [ "main" ] paths-ignore: - '.gitignore' @@ -14,8 +14,6 @@ on: - 'README.md' - '.pre-commit-config.yaml' - 'jqmc_workflow/**' - pull_request: - branches: [ "main" ] jobs: run: @@ -73,6 +71,7 @@ jobs: pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -s -v tests/test_mixed_precision.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc (inter-software comparisons) run: | @@ -86,12 +85,6 @@ jobs: pytest -s -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - #- name: Test jqmc (QMC kernels with 2MPIs) - # run: | - # mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py - # mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py - # mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py - - name: Test jqmc-tool (toolset for jqmc) run: | pytest -s -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append diff --git a/.github/workflows/jqmc-run-rc-pytest.yml b/.github/workflows/jqmc-run-rc-pytest.yml index 0f0b7687..76785a10 100644 --- a/.github/workflows/jqmc-run-rc-pytest.yml +++ b/.github/workflows/jqmc-run-rc-pytest.yml @@ -1,6 +1,6 @@ -# A full test of jqmc. +# An rc test of jqmc. -name: jqmc full test +name: jqmc rc test on: push: @@ -16,6 +16,15 @@ on: - 'jqmc_workflow/**' pull_request: branches: [ "rc" ] + paths-ignore: + - '.gitignore' + - '.github/**' + - 'doc/**' + - 'examples/**' + - 'benchmarks/**' + - 'README.md' + - '.pre-commit-config.yaml' + - 'jqmc_workflow/**' jobs: run: @@ -49,19 +58,34 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - - name: Test jqmc (QMC kernels without MPI) + - name: Test jqmc command-line run: | pytest -s -v tests/test_jqmc_command_lines.py + + - name: Test jqmc FP64 (QMC kernels without MPI, FP64) + run: | pytest -s -v tests/test_jqmc_mcmc.py pytest -s -v tests/test_jqmc_gfmc_tau.py pytest -s -v tests/test_jqmc_gfmc_bra.py - - name: Test jqmc (QMC kernels with 2MPIs) + - name: Test jqmc FP64 (QMC kernels with 2MPIs, FP64) run: | mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py + - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) + run: | + pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed + pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + + - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) + run: | + mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed + mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + - name: Test jqmc-tool (toolset for jqmc) run: | pytest -s -v tests/test_jqmc_tool.py diff --git a/.github/workflows/jqmc-run-short-pytest.yml b/.github/workflows/jqmc-run-short-pytest.yml index 23c7bb26..c7adc1ea 100644 --- a/.github/workflows/jqmc-run-short-pytest.yml +++ b/.github/workflows/jqmc-run-short-pytest.yml @@ -14,6 +14,17 @@ on: - 'README.md' - '.pre-commit-config.yaml' - 'jqmc_workflow/**' + pull_request: + branches: [ "devel*" ] + paths-ignore: + - '.gitignore' + - '.github/**' + - 'doc/**' + - 'examples/**' + - 'benchmarks/**' + - 'README.md' + - '.pre-commit-config.yaml' + - 'jqmc_workflow/**' jobs: run: @@ -67,6 +78,7 @@ jobs: pytest -s -v tests/test_swct.py --skip-heavy pytest -s -v tests/test_mcmc_force.py --skip-heavy pytest -s -v tests/test_lrdmc_force.py --skip-heavy + pytest -s -v tests/test_mixed_precision.py --precision-mode=mixed --skip-heavy - name: Test jqmc (pytest) with @jit decorator (inter-software comparisons) run: | From e7103b872c6913df2f550637a3dd1589ad020144 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:30:18 +0900 Subject: [PATCH 11/97] A trivial bug fixed in tests/test_checkpoint_mcmc.py --- tests/test_checkpoint_mcmc.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_checkpoint_mcmc.py b/tests/test_checkpoint_mcmc.py index 2ab81dcf..eaa790ab 100644 --- a/tests/test_checkpoint_mcmc.py +++ b/tests/test_checkpoint_mcmc.py @@ -307,10 +307,20 @@ class TestMCMCOptaxRoundtrip: """Optax optimizer state round-trip through MCMC save/load.""" @pytest.fixture(autouse=True) - def _setup(self, trexio_file, jastrow_combo, tmp_path): - """Build hamiltonian_data once per test method.""" + def _setup(self, trexio_file, jastrow_combo, tmp_path, monkeypatch): + """Build hamiltonian_data once per test method. + + ``run_optimize`` writes ``hamiltonian_data_opt_step_*.h5`` checkpoint + files relative to the current working directory. Without isolating + the CWD per test, parametrized runs (especially under pytest-xdist) + race on the same filename and h5py raises + ``BlockingIOError: Resource temporarily unavailable`` from the + underlying file lock. Switch CWD to the per-test ``tmp_path`` so + each test owns its own checkpoint files. + """ self.hd = _build_hamiltonian(trexio_file, jastrow_combo) self.tmp_path = tmp_path + monkeypatch.chdir(tmp_path) def test_optax_adam_state_roundtrip(self): """After 1 optax optimization step, optimizer_runtime survives save→load.""" From 89ab52da38b82a18b946318e6c618b8613dacb39 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:46:02 +0900 Subject: [PATCH 12/97] Update logger and comments --- jqmc/_precision.py | 6 ++++++ jqmc/jqmc_cli.py | 33 +++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/jqmc/_precision.py b/jqmc/_precision.py index 6ff294cd..6ffbb7d9 100644 --- a/jqmc/_precision.py +++ b/jqmc/_precision.py @@ -373,6 +373,12 @@ def _compute_AOs_kernel(aos_data, r_carts): # Strings are stored (not numpy/jax dtype types) so the str -> jnp.* / np.* # conversion lives inside the per-flavor accessors below. This keeps the # concrete dtype flavor (jnp vs np) cleanly separated at the API boundary. +# +# This module-level dict is the **single source of truth** for the precision +# state within a Python process: ``configure(mode)`` clears and refills it, +# and ``get_dtype_jnp`` / ``get_dtype_np`` read from it. No other variable +# (class attribute, environment variable, etc.) holds the active dtype +# mapping — this dict is the only place to consult or mutate. _zone_dtypes: dict[str, str] = {} diff --git a/jqmc/jqmc_cli.py b/jqmc/jqmc_cli.py index 8c3da3c3..2c17a229 100644 --- a/jqmc/jqmc_cli.py +++ b/jqmc/jqmc_cli.py @@ -35,7 +35,7 @@ # python modules import os import sys -from logging import FileHandler, Formatter, StreamHandler, getLogger +from logging import DEBUG, FileHandler, Formatter, StreamHandler, getLogger import jax import toml @@ -48,6 +48,7 @@ # jQMC from ._header_footer import _print_footer, _print_header +from ._jqmc_utility import num_sep_line from ._precision import configure as configure_precision from ._precision import mode_label as precision_mode_label from ._precision import zone_detail as precision_zone_detail @@ -69,6 +70,25 @@ logger = getLogger("jqmc").getChild(__name__) +def _log_precision_section() -> None: + """Log the active precision configuration as a labeled section. + + Output format mirrors the ``hamiltonian_data`` info section: a title + line, top ``=`` separator, the summary at INFO level, the per-zone + detail at DEBUG level, and a bottom ``=`` separator. + """ + logger.info("=" * num_sep_line) + logger.info("Printing out precision information.") + logger.info("=" * num_sep_line) + logger.info("Precision: %s", precision_mode_label()) + if logger.isEnabledFor(DEBUG): + logger.debug("Zone detail:") + for line in precision_zone_detail().split("\n"): + logger.debug(line) + logger.info("=" * num_sep_line) + logger.info("") + + def _cli(): """Main function.""" if len(sys.argv) == 1: @@ -259,9 +279,6 @@ def _cli(): sorted(extra_keys), ) configure_precision(precision_mode) - logger.info("Precision: %s", precision_mode_label()) - logger.debug("Precision zone detail:\n%s", precision_zone_detail()) - logger.info("") # default parameters parameters = cli_parameters.copy() @@ -337,6 +354,8 @@ def _cli(): comput_log_WF_param_deriv=parameter_derivatives, use_swct=use_swct, ) + _log_precision_section() + logger.info("=" * num_sep_line) logger.info("Printing out information in hamitonian_data instance.") mcmc.hamiltonian_data._logger_info() mcmc.run(num_mcmc_steps=num_mcmc_steps, max_time=max_time) @@ -469,6 +488,8 @@ def _cli(): comput_log_WF_param_deriv=True, comput_e_L_param_deriv=_need_eL_deriv, ) + _log_precision_section() + logger.info("=" * num_sep_line) logger.info("Printing out information in hamitonian_data instance.") mcmc.hamiltonian_data._logger_info() mcmc.run_optimize( @@ -580,6 +601,8 @@ def _cli(): epsilon_PW=epsilon_PW, use_swct=use_swct, ) + _log_precision_section() + logger.info("=" * num_sep_line) logger.info("Printing out information in hamitonian_data instance.") lrdmc.hamiltonian_data._logger_info() lrdmc.run(num_mcmc_steps=num_mcmc_steps, max_time=max_time) @@ -682,6 +705,8 @@ def _cli(): epsilon_PW=epsilon_PW, use_swct=use_swct, ) + _log_precision_section() + logger.info("=" * num_sep_line) logger.info("Printing out information in hamitonian_data instance.") lrdmc.hamiltonian_data._logger_info() lrdmc.run(num_mcmc_steps=num_mcmc_steps, max_time=max_time) From 19f6e31ec64676162112d8efce6469476aca17f6 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:16:20 +0900 Subject: [PATCH 13/97] Split ao_grad_lap and mo_grad_lap zones for finer-grained dtype control Precision zone changes (`jqmc/_precision.py`): - `ao_grad_lap` -> `ao_grad` (fp32 in mixed) + `ao_lap` (fp64 in mixed) - `mo_grad_lap` -> `mo_grad` (fp64) + `mo_lap` (fp64) - `jastrow_grad_lap`: kept as a single zone (fp32 in mixed) - `det_grad_lap`: kept as a single zone (fp64 in mixed) `ao` is split with an actual dtype difference because the analytic Laplacian kernel contains catastrophic-cancellation terms that fp32 cannot resolve, while the gradient kernel is safe in fp32 (see `bug/fp32/diag_07_ao_grad_vs_lap_split.py` for the diagnostic). The shared helper `_compute_S_l_m_and_grad_lap` is pinned to `ao_lap` (fp64), and `_compute_AOs_grad_analytic_sphe` downcasts the gradient output at the use site per Principle 3b. --- doc/notes/mixed_precision.md | 23 +++++++++--- jqmc/_precision.py | 69 ++++++++++++++++++++++++++--------- jqmc/atomic_orbital.py | 33 ++++++++++++----- jqmc/molecular_orbital.py | 15 ++++---- tests/test_AOs.py | 12 +++--- tests/test_MOs.py | 32 +++++++++------- tests/test_determinant.py | 18 +++++---- tests/test_mixed_precision.py | 28 ++++++++++---- 8 files changed, 156 insertions(+), 74 deletions(-) diff --git a/doc/notes/mixed_precision.md b/doc/notes/mixed_precision.md index 33d28936..db612c0b 100644 --- a/doc/notes/mixed_precision.md +++ b/doc/notes/mixed_precision.md @@ -30,7 +30,7 @@ Or keep the default (all float64, backward compatible): ## Precision zones -jQMC divides the computation into 16 **Precision Zones**. Each zone is +jQMC divides the computation into 18 **Precision Zones**. Each zone is owned by exactly one module and is named for its *purpose* (not its dtype). The mapping from zone to dtype is determined entirely by the chosen mode. @@ -38,9 +38,11 @@ chosen mode. | Zone | Owning module | `full` | `mixed` | risk | E_L path | |--------------------|------------------------|--------|----------|----------|------------| | `ao_eval` | `atomic_orbital` | f64 | **f32** | low | core | -| `ao_grad_lap` | `atomic_orbital` | f64 | **f32** | low | core | +| `ao_grad` | `atomic_orbital` | f64 | **f32** | low | core | +| `ao_lap` | `atomic_orbital` | f64 | f64 | high§ | core | | `mo_eval` | `molecular_orbital` | f64 | f64 | high\* | core | -| `mo_grad_lap` | `molecular_orbital` | f64 | f64 | high | core | +| `mo_grad` | `molecular_orbital` | f64 | f64 | high | core | +| `mo_lap` | `molecular_orbital` | f64 | f64 | high | core | | `jastrow_eval` | `jastrow_factor` | f64 | **f32** | low | core† | | `jastrow_grad_lap` | `jastrow_factor` | f64 | **f32** | low | core | | `jastrow_ratio` | `jastrow_factor` | f64 | **f32** | low | indirect‡ | @@ -69,6 +71,17 @@ ECP non-local potential, which evaluates `Psi(R')/Psi(R)` on a quadrature grid via rank-1 ratio updates. In non-ECP systems these zones have no E_L impact. +§ `ao_lap` is kept fp64 in `mixed` mode because the analytic Laplacian +formula contains catastrophic-cancellation terms of the form +`4 Z² r² − 6 Z` and `(safe_div − 2 Z·base)² − safe_div² − 2 Z` that +amplify fp32 round-off into a force bias of order ~1 Ha/bohr in N₂ +(diagnostic `bug/fp32/diag_07_ao_grad_vs_lap_split.py`). The grad +counterpart `ao_grad` has no such cancellation and is safe at fp32 +(max|dF| ≈ 5e-6 Ha/bohr). This is the only zone pair in jQMC where the +grad and Laplacian halves take different dtypes, motivating the split +of the original `ao_grad_lap` zone into separate `ao_grad` / `ao_lap` +zones. + ## Workflow integration When using `jqmc_workflow`, pass the precision mode to any workflow class: @@ -100,8 +113,8 @@ module. The mapping zone ↔ owning module is one-to-one. **Principle 2 — A module may own multiple Precision Zones.** Different code paths in the same module legitimately need different -precisions (e.g. `ao_eval` vs `ao_grad_lap`, or `det_eval` vs `det_ratio`). -Each zone is named for its *purpose*, not for its dtype. +precisions (e.g. `ao_eval` vs `ao_grad` vs `ao_lap`, or `det_eval` vs +`det_ratio`). Each zone is named for its *purpose*, not for its dtype. **Principle 3 — Cast responsibility lies with the function that does arithmetic on the value, never with passthrough wrappers.** diff --git a/jqmc/_precision.py b/jqmc/_precision.py index 6ffbb7d9..eff8d465 100644 --- a/jqmc/_precision.py +++ b/jqmc/_precision.py @@ -20,7 +20,7 @@ Principle 2 — A module may own multiple Precision Zones. ------------------------------------------------------------ Different code paths in the same module legitimately need different precisions -(e.g. ``ao_eval`` vs ``ao_grad_lap``, or ``det_eval`` vs ``det_ratio``). Each +(e.g. ``ao_eval`` vs ``ao_grad``, or ``det_eval`` vs ``det_ratio``). Each zone is named for its *purpose*, not for its dtype. ------------------------------------------------------------ @@ -174,9 +174,11 @@ def compute_coulomb(r_carts, R_carts): Zone Owning module Default Mixed risk E_L path ================== ================================= ========= ======== ===== ========= ``ao_eval`` atomic_orbital.py (forward) float64 float32 low core -``ao_grad_lap`` atomic_orbital.py (grad/lap) float64 float32 low core +``ao_grad`` atomic_orbital.py (gradient) float64 float32 low core +``ao_lap`` atomic_orbital.py (Laplacian) float64 float64 high§ core ``mo_eval`` molecular_orbital.py (forward) float64 float64 high* core -``mo_grad_lap`` molecular_orbital.py (grad/lap) float64 float64 high core +``mo_grad`` molecular_orbital.py (gradient) float64 float64 high core +``mo_lap`` molecular_orbital.py (Laplacian) float64 float64 high core ``jastrow_eval`` jastrow_factor.py (forward) float64 float32 low core† ``jastrow_grad_lap`` jastrow_factor.py (grad/lap) float64 float32 low core ``jastrow_ratio`` jastrow_factor.py (ratio update) float64 float32 low indirect‡ @@ -201,6 +203,16 @@ def compute_coulomb(r_carts, R_carts): (E_L depends on *derivatives* of ln|Psi|). Diagnostics show zero E_L bias when these zones alone are fp32. +§ ``ao_lap`` is fp64 even in mixed mode because the analytic Laplacian +kernel for spherical AOs contains catastrophic cancellation +(``4 Z² r² − 6 Z`` and ``(safe_div − 2 Z·base)² − safe_div² − 2 Z`` +terms) that fp32 cannot resolve for tight Gaussians. Diagnostic +``bug/fp32/diag_07_ao_grad_vs_lap_split.py`` showed that +``ao_lap=fp32`` alone reproduces the full atomic-force bias +(``max|dF| ≈ 1.9 Ha/bohr`` on N₂ at scale=0.3, ``≈ 2e−2 Ha/bohr`` on +the water-cluster-8 system), while ``ao_grad=fp32`` alone is safe +(``max|dF| < 8e−3 Ha/bohr``). + ‡ ``det_ratio`` and ``jastrow_ratio`` affect E_L **indirectly** through the ECP non-local potential, which evaluates Psi(R')/Psi(R) on a quadrature grid via rank-1 ratio updates (see @@ -225,7 +237,7 @@ def _compute_AOs_kernel(aos_data, r_carts): # NOTE: never reach for another module's zone (e.g. # ``get_dtype_jnp("local_energy")``) here — that violates # Principle 1 (zone ↔ owning module is 1:1). atomic_orbital.py - # may only consult ao_eval / ao_grad_lap. + # may only consult ao_eval / ao_grad / ao_lap. dtype_jnp = get_dtype_jnp("ao_eval") R_carts = aos_data._atomic_center_carts_jnp diff = (r_carts - R_carts).astype(dtype_jnp) @@ -276,10 +288,12 @@ def _compute_AOs_kernel(aos_data, r_carts): _FULL_PRECISION: dict[str, str] = { # atomic_orbital.py "ao_eval": "float64", # AO forward evaluation - "ao_grad_lap": "float64", # AO gradient / Laplacian + "ao_grad": "float64", # AO gradient + "ao_lap": "float64", # AO Laplacian # molecular_orbital.py "mo_eval": "float64", # MO forward evaluation (mo_coef @ AO) - "mo_grad_lap": "float64", # MO gradient / Laplacian + "mo_grad": "float64", # MO gradient + "mo_lap": "float64", # MO Laplacian # jastrow_factor.py "jastrow_eval": "float64", # Jastrow factor (J1/J2/J3) "jastrow_grad_lap": "float64", # Jastrow gradient / Laplacian @@ -307,13 +321,19 @@ def _compute_AOs_kernel(aos_data, r_carts): # The downstream consumer (mo_eval / det_eval / # jastrow_eval) is fp64 and explicitly casts the AO # result up before any sensitive arithmetic. -# ao_grad_lap - AO gradient / Laplacian kernel; same O(N_ao × N_e) -# cost as ao_eval. Diagnostics show bias < 6e-05 Ha -# at 32 electrons (0.05 kcal/mol margin ×1.3). +# ao_grad - AO analytic gradient kernel; same O(N_ao × N_e) +# cost as ao_eval. Diagnostics +# (bug/fp32/diag_07) show grad-only fp32 yields +# max|dF| < 8e-3 Ha/bohr (relative bias ~5e-5 on +# water-cluster-8) — well within chemical accuracy. # jastrow_eval - smooth correlation function value (pre-exp). -# jastrow_grad_lap - nabla J, nabla^2 J; Jastrow is a smooth function -# with low cancellation. Diagnostics show bias -# < 8e-06 Ha at 32 electrons (0.05 kcal/mol margin ×11). +# jastrow_grad_lap - nabla J, nabla^2 J; smooth Jastrow factor, low +# cancellation. Diagnostics show bias < 8e-06 Ha +# at 32 electrons (0.05 kcal/mol margin ×11). +# Kept as a single zone (no grad/lap split) because +# both halves share the same fp32 risk profile and +# Jastrow grad/lap functions compute the two +# together (``compute_grads_and_laplacian_*``). # jastrow_ratio - J(R')-J(R) log-ratio; smooth and well-behaved. # Diagnostics show bias < 2e-06 Ha (margin ×44). # @@ -322,6 +342,12 @@ def _compute_AOs_kernel(aos_data, r_carts): # unacceptable bias on E_L for ~32-electron systems, OR the # kernel is cheap enough that fp32 is not worth the bias: # +# ao_lap - analytic Laplacian kernel for spherical/Cartesian AOs +# contains catastrophic cancellation (``4 Z² r² − 6 Z`` +# and ``(safe_div − 2 Z·base)² − safe_div² − 2 Z``). +# diag_07 showed lap=fp32 alone yields max|dF| ≈ 1.9 +# Ha/bohr on N₂ (scale=0.3), reproducing the entire +# bias of grad+lap=fp32. fp64 mandatory. # coulomb - sum of 1/r + ECP spherical quadrature. Cheap # (O(N_e^2) el-el + O(N_e * N_nuc) el-ion, vs # O(N_e * N_ao) AO eval) but contributes the @@ -333,8 +359,15 @@ def _compute_AOs_kernel(aos_data, r_carts): # det_eval - geminal matrix + log(det) + SVD; cancellation in # log(det), SVD 1/s near-singular, ε≈1e-7 entries # produce O(1) log|det| error. -# *_grad_lap - second derivatives of ln|Psi|; cancellation-sensitive -# (except ao_grad_lap and jastrow_grad_lap — smooth kernels). +# mo_grad / mo_lap / det_grad_lap +# - second derivatives of ln|Psi|; cancellation-sensitive +# on the determinant side (the AO-side fp32 is absorbed +# by the fp64 mo_coef matmul). jastrow_grad_lap is the +# exception (smooth Jastrow, no severe cancellation). +# det_grad_lap is kept as a single zone (no grad/lap +# split) for symmetry with jastrow_grad_lap and because +# the determinant grad/lap functions naturally compute +# both quantities together (``compute_grads_and_laplacian_*``). # wf_kinetic - sum (lap_J + lap_lnD) + |grad_J + grad_lnD|^2; cancellation. # local_energy - T + V assembly; small differences between large terms. # det_ratio - SM rank-1 ratio used by MCMC accept/reject AND @@ -343,17 +376,19 @@ def _compute_AOs_kernel(aos_data, r_carts): _MIXED_PRECISION: dict[str, str] = { # atomic_orbital.py "ao_eval": "float32", # low risk (heavy kernel) - "ao_grad_lap": "float32", # low risk (bias < 6e-05 Ha at 32e; heavy kernel) + "ao_grad": "float32", # low risk (smooth grad kernel; bias < 8e-3 Ha/bohr atomic force) + "ao_lap": "float64", # high risk (catastrophic cancellation in 4Z²r²-6Z terms) # molecular_orbital.py "mo_eval": "float64", # high risk (feeds det_eval) - "mo_grad_lap": "float64", # high risk + "mo_grad": "float64", # high risk + "mo_lap": "float64", # high risk # jastrow_factor.py "jastrow_eval": "float32", # low risk "jastrow_grad_lap": "float32", # low risk (smooth J; bias < 8e-06 Ha at 32e) "jastrow_ratio": "float32", # low risk (smooth J ratio; bias < 2e-06 Ha at 32e) # determinant.py "det_eval": "float64", # high risk (LU/det / SVD) - "det_grad_lap": "float64", # high risk + "det_grad_lap": "float64", # high risk (kept unsplit for symmetry with jastrow) "det_ratio": "float64", # high risk (SM update error + ECP non-local ratio) # coulomb_potential.py "coulomb": "float64", # cheap kernel + largest single fp32 bias (~6e-5 Ha) diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index b91c3148..ed34dc05 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -2329,11 +2329,21 @@ def lnorm(l): def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Vectorized solid harmonics values, gradients, and Laplacians. + Pinned to the ``ao_lap`` zone (fp64 in mixed mode). The gradient + consumer (``_compute_AOs_grad_analytic_sphe``) lives in the + ``ao_grad`` zone (fp32 in mixed mode); it is responsible for + down-casting the grad output of this helper to its own zone at the + use site (Principle 3b). Running the helper at the higher of the + two precisions is intentional — the solid-harmonics polynomial + expansion is cheap (49 × num_R × num_e) compared to the contracted + AO formulas, so the perf cost of fp64 here is small while keeping + the laplacian path numerically safe. + Returns: tuple: (values, grads, laps) where values has shape (49, num_R, num_r), grads has shape (49, num_R, num_r, 3), and laps has shape (49, num_R, num_r). """ - dtype_jnp = get_dtype_jnp("ao_grad_lap") + dtype_jnp = get_dtype_jnp("ao_lap") S_L_M_COEFFS = ( jnp.array([1.0], dtype=dtype_jnp), jnp.array([1.0], dtype=dtype_jnp), @@ -2580,9 +2590,9 @@ def _single_val_grad_lap(diff: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.A @jit def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for Cartesian AOs (contracted).""" - dtype_jnp = get_dtype_jnp("ao_grad_lap") + dtype_jnp = get_dtype_jnp("ao_lap") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) - # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). + # via JAX promotion, then downcast to the ao_lap zone (Principle 3b). # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage # accessor on the basis-data dataclass. R_carts = aos_data._atomic_center_carts_prim_jnp @@ -2629,9 +2639,9 @@ def _second_component(base, n): @jit def _compute_AOs_laplacian_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for spherical AOs (contracted).""" - dtype_jnp = get_dtype_jnp("ao_grad_lap") + dtype_jnp = get_dtype_jnp("ao_lap") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) - # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). + # via JAX promotion, then downcast to the ao_lap zone (Principle 3b). # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage # accessor on the basis-data dataclass. R_carts = aos_data._atomic_center_carts_prim_jnp @@ -2892,9 +2902,9 @@ def _compute_AOs_laplacian_debug( @jit def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for Cartesian AOs (contracted).""" - dtype_jnp = get_dtype_jnp("ao_grad_lap") + dtype_jnp = get_dtype_jnp("ao_grad") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) - # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). + # via JAX promotion, then downcast to the ao_grad zone (Principle 3b). # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage # accessor on the basis-data dataclass. R_carts = aos_data._atomic_center_carts_prim_jnp @@ -2944,9 +2954,9 @@ def _grad_component(base, n): @jit def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for spherical AOs (contracted).""" - dtype_jnp = get_dtype_jnp("ao_grad_lap") + dtype_jnp = get_dtype_jnp("ao_grad") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) - # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). + # via JAX promotion, then downcast to the ao_grad zone (Principle 3b). # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage # accessor on the basis-data dataclass. R_carts = aos_data._atomic_center_carts_prim_jnp @@ -2973,7 +2983,12 @@ def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarra R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) max_ml, S_l_m_dup_all_l_m = _compute_S_l_m(r_R_diffs_uq) + # ``_compute_S_l_m_and_grad_lap`` is pinned to ``ao_lap`` (fp64); its + # grad output is therefore returned in fp64. Cast it down to the + # ``ao_grad`` zone at the use site (Principle 3b) so the contracted + # grad arithmetic below stays in this function's own zone. _, S_l_m_grad_all_l_m, _ = _compute_S_l_m_and_grad_lap(r_R_diffs_uq) + S_l_m_grad_all_l_m = S_l_m_grad_all_l_m.astype(dtype_jnp) S_l_m_dup_all_l_m_reshaped = S_l_m_dup_all_l_m.reshape( (S_l_m_dup_all_l_m.shape[0] * S_l_m_dup_all_l_m.shape[1], S_l_m_dup_all_l_m.shape[2]), order="F" diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index 2b635550..757b16dc 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -2,7 +2,8 @@ Precision Zones: - ``mo_eval``: forward MO evaluation (compute_MOs). - - ``mo_grad_lap``: MO gradient and Laplacian (compute_MOs_grad, compute_MOs_laplacian). + - ``mo_grad``: MO gradient (compute_MOs_grad). + - ``mo_lap``: MO Laplacian (compute_MOs_laplacian). See :mod:`jqmc._precision` for details. """ @@ -285,10 +286,10 @@ def compute_MOs_laplacian(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: Returns: jax.Array: Laplacians of each MO, shape ``(num_mo, N_e)``. """ - dtype_jnp = get_dtype_jnp("mo_grad_lap") + dtype_jnp = get_dtype_jnp("mo_lap") mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) ao_lap = compute_AOs_laplacian(mos_data.aos_data, r_carts) - # ao_lap lives in the ao_grad_lap zone; cast to mo_grad_lap at the use site + # ao_lap lives in the ao_lap zone; cast to mo_lap at the use site # (Principle 3b — cast operands to this function's own zone immediately # before consuming them as arithmetic operands). return jnp.dot(mo_coefficients, ao_lap.astype(dtype_jnp)) @@ -352,10 +353,10 @@ def compute_MOs_grad( tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64]]: Gradients per component ``(grad_x, grad_y, grad_z)``, each of shape ``(num_mo, N_e)``. """ - dtype_jnp = get_dtype_jnp("mo_grad_lap") + dtype_jnp = get_dtype_jnp("mo_grad") mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z = compute_AOs_grad(mos_data.aos_data, r_carts) - # AO gradient outputs live in the ao_grad_lap zone; cast to mo_grad_lap at the + # AO gradient outputs live in the ao_grad zone; cast to mo_grad at the # use site (Principle 3b — cast operands to this function's own zone immediately # before consuming them as arithmetic operands). mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x.astype(dtype_jnp)) @@ -374,10 +375,10 @@ def _compute_MOs_grad_autodiff( npt.NDArray[np.float64], ]: """This method is for computing the gradients (x,y,z) of the given molecular orbital at r_carts.""" - dtype_jnp = get_dtype_jnp("mo_grad_lap") + dtype_jnp = get_dtype_jnp("mo_grad") mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z = _compute_AOs_grad_autodiff(mos_data.aos_data, r_carts) - # AO gradient outputs live in the ao_grad_lap zone; cast to mo_grad_lap at the + # AO gradient outputs live in the ao_grad zone; cast to mo_grad at the # use site (Principle 3b). mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x.astype(dtype_jnp)) mo_matrix_grad_y = jnp.dot(mo_coefficients, mo_matrix_grad_y.astype(dtype_jnp)) diff --git a/tests/test_AOs.py b/tests/test_AOs.py index 0e08b436..de412595 100755 --- a/tests/test_AOs.py +++ b/tests/test_AOs.py @@ -504,7 +504,7 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_auto(): gx_auto, gy_auto, gz_auto = _compute_AOs_grad_autodiff(aos_data=aos_data, r_carts=r_carts) gx_an, gy_an, gz_an = compute_AOs_grad(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("ao_grad_lap", "strict") + atol, rtol = get_tolerance("ao_grad", "strict") assert not np.any(np.isnan(np.asarray(gx_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_auto))), "NaN detected in second argument" np.testing.assert_allclose(gx_an, gx_auto, atol=atol, rtol=rtol) @@ -567,7 +567,7 @@ def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): gx_auto_cart, gy_auto_cart, gz_auto_cart = _compute_AOs_grad_autodiff(aos_data=aos_data_cart, r_carts=r_carts) gx_num_cart, gy_num_cart, gz_num_cart = _compute_AOs_grad_debug(aos_data=aos_data_cart, r_carts=r_carts) - atol, rtol = get_tolerance("ao_grad_lap", "loose") + atol, rtol = get_tolerance("ao_grad", "loose") assert not np.any(np.isnan(np.asarray(gx_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(gx_auto_cart, gx_num_cart, atol=atol, rtol=rtol) @@ -755,7 +755,7 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_numerical(): gx_num_cart, gy_num_cart, gz_num_cart = _compute_AOs_grad_debug(aos_data=aos_data, r_carts=r_carts) gx_an_cart, gy_an_cart, gz_an_cart = compute_AOs_grad(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("ao_grad_lap", "loose") + atol, rtol = get_tolerance("ao_grad", "loose") assert not np.any(np.isnan(np.asarray(gx_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(gx_an_cart, gx_num_cart, atol=atol, rtol=rtol) @@ -867,7 +867,7 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_auto(): lap_auto_cart = _compute_AOs_laplacian_autodiff(aos_data=aos_data, r_carts=r_carts) lap_an_cart = compute_AOs_laplacian(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("ao_grad_lap", "strict") + atol, rtol = get_tolerance("ao_lap", "strict") assert not np.any(np.isnan(np.asarray(lap_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_auto_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_an_cart, lap_auto_cart, atol=atol, rtol=rtol) @@ -965,7 +965,7 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_numerical(): lap_num_cart = _compute_AOs_laplacian_debug(aos_data=aos_data, r_carts=r_carts) lap_an_cart = compute_AOs_laplacian(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("ao_grad_lap", "loose") + atol, rtol = get_tolerance("ao_lap", "loose") assert not np.any(np.isnan(np.asarray(lap_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_an_cart, lap_num_cart, atol=atol, rtol=rtol) @@ -1065,7 +1065,7 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): lap_num_cart = _compute_AOs_laplacian_autodiff(aos_data=aos_data, r_carts=r_carts) lap_auto_cart = _compute_AOs_laplacian_debug(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("ao_grad_lap", "loose") + atol, rtol = get_tolerance("ao_lap", "loose") assert not np.any(np.isnan(np.asarray(lap_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_auto_cart, lap_num_cart, atol=atol, rtol=rtol) diff --git a/tests/test_MOs.py b/tests/test_MOs.py index 4ab5b870..76589e4f 100755 --- a/tests/test_MOs.py +++ b/tests/test_MOs.py @@ -251,7 +251,7 @@ def test_MOs_comparing_auto_and_numerical_grads(): mo_matrix_grad_z_numerical, ) = _compute_MOs_grad_autodiff(mos_data=mos_data, r_carts=r_carts) - atol, rtol = get_tolerance("mo_grad_lap", "loose") + atol, rtol = get_tolerance("mo_grad", "loose") assert not np.any(np.isnan(np.asarray(mo_matrix_grad_x_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_grad_x_numerical))), "NaN detected in second argument" np.testing.assert_allclose(mo_matrix_grad_x_auto, mo_matrix_grad_x_numerical, atol=atol, rtol=rtol) @@ -388,7 +388,7 @@ def test_MOs_comparing_auto_and_numerical_laplacians(): mo_matrix_laplacian_auto = _compute_MOs_laplacian_autodiff(mos_data=mos_data, r_carts=r_carts) - atol, rtol = get_tolerance("mo_grad_lap", "loose") + atol, rtol = get_tolerance("mo_lap", "loose") assert not np.any(np.isnan(np.asarray(mo_matrix_laplacian_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_laplacian_numerical))), "NaN detected in second argument" np.testing.assert_allclose( @@ -454,8 +454,8 @@ def test_MOs_comparing_analytic_and_auto_grads(): grad_x_auto, grad_y_auto, grad_z_auto = _compute_MOs_grad_autodiff(mos_data=mos_data, r_carts=r_carts) - # Path crosses ao_grad_lap (fp32 in mixed) -> mo_grad_lap (fp64); use min. - atol, rtol = get_tolerance_min(["ao_grad_lap", "mo_grad_lap"], "strict") + # Path crosses ao_grad (fp32 in mixed) -> mo_grad (fp64); use min. + atol, rtol = get_tolerance_min(["ao_grad", "mo_grad"], "strict") assert not np.any(np.isnan(np.asarray(grad_x_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_x_auto))), "NaN detected in second argument" np.testing.assert_allclose(grad_x_an, grad_x_auto, atol=atol, rtol=rtol) @@ -522,8 +522,8 @@ def test_MOs_comparing_analytic_and_auto_laplacians(): mo_lap_auto = _compute_MOs_laplacian_autodiff(mos_data=mos_data, r_carts=r_carts) - # Path crosses ao_grad_lap (fp32 in mixed) -> mo_grad_lap (fp64); use min. - atol, rtol = get_tolerance_min(["ao_grad_lap", "mo_grad_lap"], "strict") + # Path crosses ao_lap (fp64) -> mo_lap (fp64); use min. + atol, rtol = get_tolerance_min(["ao_lap", "mo_lap"], "strict") assert not np.any(np.isnan(np.asarray(mo_lap_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_lap_auto))), "NaN detected in second argument" np.testing.assert_allclose(mo_lap_an, mo_lap_auto, atol=atol, rtol=rtol) @@ -603,19 +603,21 @@ def test_MOs_sphe_to_cart(): grad_sphe = compute_MOs_grad(mos_data=mos_sphe, r_carts=r_carts) grad_cart = compute_MOs_grad(mos_data=mos_cart, r_carts=r_carts) - # grad/lap path crosses ao_grad_lap (fp32 in mixed) -> mo_grad_lap. - atol_gl, rtol_gl = get_tolerance_min(["ao_grad_lap", "mo_grad_lap"], "strict") + # grad path crosses ao_grad (fp32 in mixed) -> mo_grad. + atol_g, rtol_g = get_tolerance_min(["ao_grad", "mo_grad"], "strict") for g_cart, g_sphe in zip(grad_cart, grad_sphe, strict=True): assert not np.any(np.isnan(np.asarray(g_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(g_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(g_cart, g_sphe, atol=atol_gl, rtol=rtol_gl) + np.testing.assert_allclose(g_cart, g_sphe, atol=atol_g, rtol=rtol_g) lap_sphe = compute_MOs_laplacian(mos_data=mos_sphe, r_carts=r_carts) lap_cart = compute_MOs_laplacian(mos_data=mos_cart, r_carts=r_carts) + # lap path crosses ao_lap (fp64) -> mo_lap (fp64). + atol_l, rtol_l = get_tolerance_min(["ao_lap", "mo_lap"], "strict") assert not np.any(np.isnan(np.asarray(lap_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_sphe))), "NaN detected in second argument" - np.testing.assert_allclose(lap_cart, lap_sphe, atol=atol_gl, rtol=rtol_gl) + np.testing.assert_allclose(lap_cart, lap_sphe, atol=atol_l, rtol=rtol_l) jax.clear_caches() @@ -708,19 +710,21 @@ def test_MOs_cart_to_sphe(): grad_cart = compute_MOs_grad(mos_data=mos_cart, r_carts=r_carts) grad_sphe = compute_MOs_grad(mos_data=mos_sphe, r_carts=r_carts) - # grad/lap path crosses ao_grad_lap (fp32 in mixed) -> mo_grad_lap. - atol_gl, rtol_gl = get_tolerance_min(["ao_grad_lap", "mo_grad_lap"], "strict") + # grad path crosses ao_grad (fp32 in mixed) -> mo_grad. + atol_g, rtol_g = get_tolerance_min(["ao_grad", "mo_grad"], "strict") for g_cart, g_sphe in zip(grad_cart, grad_sphe, strict=True): assert not np.any(np.isnan(np.asarray(g_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(g_cart))), "NaN detected in second argument" - np.testing.assert_allclose(g_sphe, g_cart, atol=atol_gl, rtol=rtol_gl) + np.testing.assert_allclose(g_sphe, g_cart, atol=atol_g, rtol=rtol_g) lap_cart = compute_MOs_laplacian(mos_data=mos_cart, r_carts=r_carts) lap_sphe = compute_MOs_laplacian(mos_data=mos_sphe, r_carts=r_carts) + # lap path crosses ao_lap (fp64) -> mo_lap (fp64). + atol_l, rtol_l = get_tolerance_min(["ao_lap", "mo_lap"], "strict") assert not np.any(np.isnan(np.asarray(lap_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_cart))), "NaN detected in second argument" - np.testing.assert_allclose(lap_sphe, lap_cart, atol=atol_gl, rtol=rtol_gl) + np.testing.assert_allclose(lap_sphe, lap_cart, atol=atol_l, rtol=rtol_l) jax.clear_caches() diff --git a/tests/test_determinant.py b/tests/test_determinant.py index 7655c790..0b84ac86 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -309,9 +309,9 @@ def _build_sphe_aos_l_le6(rng: np.random.Generator) -> AOs_sphe_data: def test_geminal_sphe_to_cart_AOs_data(): """Round-trip AOs l<=6: spherical→Cartesian keeps geminal values/grads.""" - # Comparison crosses ao_eval/det_eval (values) and ao_grad_lap/det_grad_lap (grads); + # Comparison crosses ao_eval/det_eval (values) and ao_grad/ao_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. - atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad_lap", "det_grad_lap"), "strict") + atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad", "ao_lap", "det_grad_lap"), "strict") rng = np.random.default_rng(321) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -349,9 +349,9 @@ def test_geminal_sphe_to_cart_AOs_data(): def test_geminal_cart_to_sphe_AOs_data(): """Round-trip AOs l<=6: Cartesian→spherical keeps geminal values/grads.""" - # Comparison crosses ao_eval/det_eval (values) and ao_grad_lap/det_grad_lap (grads); + # Comparison crosses ao_eval/det_eval (values) and ao_grad/ao_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. - atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad_lap", "det_grad_lap"), "strict") + atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad", "ao_lap", "det_grad_lap"), "strict") rng = np.random.default_rng(654) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -391,10 +391,11 @@ def test_geminal_cart_to_sphe_AOs_data(): def test_geminal_sphe_to_cart_MOs_data(): """Round-trip MOs built on l<=6 AOs: spherical→Cartesian keeps geminal values/grads.""" - # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad_lap/mo_grad_lap/det_grad_lap (grads); + # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad/ao_lap/mo_grad/mo_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. atol_c, rtol_c = get_tolerance_min( - ("ao_eval", "mo_eval", "det_eval", "ao_grad_lap", "mo_grad_lap", "det_grad_lap"), "strict" + ("ao_eval", "mo_eval", "det_eval", "ao_grad", "ao_lap", "mo_grad", "mo_lap", "det_grad_lap"), + "strict", ) rng = np.random.default_rng(777) @@ -437,10 +438,11 @@ def test_geminal_sphe_to_cart_MOs_data(): def test_geminal_cart_to_sphe_MOs_data(): """Round-trip MOs l<=6: Cartesian→spherical keeps geminal values/grads.""" - # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad_lap/mo_grad_lap/det_grad_lap (grads); + # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad/ao_lap/mo_grad/mo_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. atol_c, rtol_c = get_tolerance_min( - ("ao_eval", "mo_eval", "det_eval", "ao_grad_lap", "mo_grad_lap", "det_grad_lap"), "strict" + ("ao_eval", "mo_eval", "det_eval", "ao_grad", "ao_lap", "mo_grad", "mo_lap", "det_grad_lap"), + "strict", ) rng = np.random.default_rng(888) diff --git a/tests/test_mixed_precision.py b/tests/test_mixed_precision.py index 95fa694d..e378aede 100644 --- a/tests/test_mixed_precision.py +++ b/tests/test_mixed_precision.py @@ -48,6 +48,7 @@ compute_geminal_all_elements, compute_geminal_dn_one_column_elements, compute_geminal_up_one_row_elements, + compute_grads_and_laplacian_ln_Det, compute_ln_det_geminal_all_elements, ) from jqmc.hamiltonians import Hamiltonian_data, compute_local_energy # noqa: E402 @@ -187,16 +188,16 @@ def test_compute_AOs_output_dtype(self, h2_data): ) def test_compute_AOs_grad_output_dtype(self, h2_data): - """compute_AOs_grad must return ao_grad_lap zone dtype.""" + """compute_AOs_grad must return ao_grad zone dtype.""" grad_x, grad_y, grad_z = compute_AOs_grad(h2_data["aos_data"], h2_data["r_up"]) - expected = get_dtype_jnp("ao_grad_lap") + expected = get_dtype_jnp("ao_grad") for name, arr in [("grad_x", grad_x), ("grad_y", grad_y), ("grad_z", grad_z)]: assert arr.dtype == expected, f"compute_AOs_grad {name} dtype is {arr.dtype}, expected {expected}." def test_compute_AOs_laplacian_output_dtype(self, h2_data): - """compute_AOs_laplacian must return ao_grad_lap zone dtype.""" + """compute_AOs_laplacian must return ao_lap zone dtype.""" lap = compute_AOs_laplacian(h2_data["aos_data"], h2_data["r_up"]) - expected = get_dtype_jnp("ao_grad_lap") + expected = get_dtype_jnp("ao_lap") assert lap.dtype == expected, f"compute_AOs_laplacian dtype is {lap.dtype}, expected {expected}." @@ -315,6 +316,17 @@ def test_ln_det_output_dtype(self, h2_data): f"compute_ln_det dtype is {jnp.asarray(ln_det).dtype}, expected {expected}." ) + def test_compute_grads_and_laplacian_ln_Det_output_dtype(self, h2_data): + """compute_grads_and_laplacian_ln_Det outputs must use det_grad_lap zone dtype.""" + grad_up, grad_dn, lap_up, lap_dn = compute_grads_and_laplacian_ln_Det( + h2_data["geminal_data"], h2_data["r_up"], h2_data["r_dn"] + ) + expected = get_dtype_jnp("det_grad_lap") + _assert_dtype(grad_up, expected, "compute_grads_and_laplacian_ln_Det grad_up") + _assert_dtype(grad_dn, expected, "compute_grads_and_laplacian_ln_Det grad_dn") + _assert_dtype(lap_up, expected, "compute_grads_and_laplacian_ln_Det lap_up") + _assert_dtype(lap_dn, expected, "compute_grads_and_laplacian_ln_Det lap_dn") + # --------------------------------------------------------------------------- # F. Coulomb zone (coulomb → float32 in mixed) @@ -414,11 +426,11 @@ def test_compute_AOs_sphe_output_dtype(self, h2_sphe_data): def test_compute_AOs_sphe_grad_output_dtype(self, h2_sphe_data): gx, gy, gz = compute_AOs_grad(h2_sphe_data["aos_data"], h2_sphe_data["r_up"]) for name, arr in [("grad_x", gx), ("grad_y", gy), ("grad_z", gz)]: - _assert_dtype(arr, get_dtype_jnp("ao_grad_lap"), f"compute_AOs_grad sphe {name}") + _assert_dtype(arr, get_dtype_jnp("ao_grad"), f"compute_AOs_grad sphe {name}") def test_compute_AOs_sphe_laplacian_output_dtype(self, h2_sphe_data): lap = compute_AOs_laplacian(h2_sphe_data["aos_data"], h2_sphe_data["r_up"]) - _assert_dtype(lap, get_dtype_jnp("ao_grad_lap"), "compute_AOs_laplacian (sphe)") + _assert_dtype(lap, get_dtype_jnp("ao_lap"), "compute_AOs_laplacian (sphe)") class TestMOExtendedDtype: @@ -426,13 +438,13 @@ class TestMOExtendedDtype: def test_compute_MOs_grad_output_dtype(self, h2_data): gx, gy, gz = compute_MOs_grad(h2_data["mos_data_up"], h2_data["r_up"]) - expected = get_dtype_jnp("mo_grad_lap") + expected = get_dtype_jnp("mo_grad") for name, arr in [("grad_x", gx), ("grad_y", gy), ("grad_z", gz)]: _assert_dtype(arr, expected, f"compute_MOs_grad {name}") def test_compute_MOs_laplacian_output_dtype(self, h2_data): lap = compute_MOs_laplacian(h2_data["mos_data_up"], h2_data["r_up"]) - _assert_dtype(lap, get_dtype_jnp("mo_grad_lap"), "compute_MOs_laplacian") + _assert_dtype(lap, get_dtype_jnp("mo_lap"), "compute_MOs_laplacian") class TestJastrowOneBodyDtype: From aa1680ccbca03c986a0d0801a51c453608d26d8f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:55:36 +0000 Subject: [PATCH 14/97] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.11 → v0.15.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.11...v0.15.12) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09d00d77..9d4ecbeb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.11 + rev: v0.15.12 hooks: #- id: ruff # args: [ "--fix", "--show-fixes" ] From 2f9c767674b9d0dae13de0e60b566492e8cba58a Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:06:06 +0900 Subject: [PATCH 15/97] Vectorize electron config generator and PRNG key init Replace the per-walker Python loop in `_generate_init_electron_configurations` with a vectorized NumPy implementation: the deterministic atom assignment is replayed once, and all spherical offsets are drawn in a single batched call. The original implementation is kept as `_generate_init_electron_configurations_debug` for tests. Replace `[fold_in(key, nw) for nw in range(num_walkers)]` with batched `jax.random.split(key, num_walkers)` in `MCMC`, `_MCMC_debug`, `GFMC_t`, `_GFMC_t_debug`, `GFMC_n`, and `_GFMC_n_debug`. Also remove the per-walker `bincount` + `logger.debug` loop from the same six `__init__` blocks. Add tests covering position uniqueness, owner/per-atom-count agreement with the reference, dimer singlet anti-alignment, and per-atom charge neutrality across all reachable `S`. --- .github/workflows/jqmc-run-full-pytest.yml | 1 + .github/workflows/jqmc-run-short-pytest.yml | 1 + jqmc/_jqmc_utility.py | 174 ++++++++++- jqmc/jqmc_gfmc.py | 112 ++++--- jqmc/jqmc_mcmc.py | 50 +-- tests/test_init_electron_configurations.py | 324 ++++++++++++++++++++ 6 files changed, 593 insertions(+), 69 deletions(-) create mode 100644 tests/test_init_electron_configurations.py diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index 2d7574a8..cee32e6c 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -57,6 +57,7 @@ jobs: - name: Test jqmc (intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail + pytest -s -v tests/test_init_electron_configurations.py --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_structure.py --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append diff --git a/.github/workflows/jqmc-run-short-pytest.yml b/.github/workflows/jqmc-run-short-pytest.yml index c7adc1ea..e36ce68f 100644 --- a/.github/workflows/jqmc-run-short-pytest.yml +++ b/.github/workflows/jqmc-run-short-pytest.yml @@ -68,6 +68,7 @@ jobs: - name: Test jqmc (pytest) with @jit decorator (intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --skip-heavy + pytest -s -v tests/test_init_electron_configurations.py --skip-heavy pytest -s -v tests/test_structure.py --skip-heavy pytest -s -v tests/test_AOs.py --skip-heavy pytest -s -v tests/test_MOs.py --skip-heavy diff --git a/jqmc/_jqmc_utility.py b/jqmc/_jqmc_utility.py index 7ed9925b..c50f40f0 100644 --- a/jqmc/_jqmc_utility.py +++ b/jqmc/_jqmc_utility.py @@ -58,7 +58,179 @@ def _generate_init_electron_configurations( charges: np.ndarray, coords: np.ndarray, ) -> tuple[npt.NDArray, npt.NDArray, npt.NDArray, npt.NDArray]: - """Generate initial electron configurations for walkers. + """Vectorized initial electron configuration generator for many walkers. + + Functionally equivalent to :func:`_generate_init_electron_configurations_debug` + but avoids the per-walker Python loop. Designed for large walker counts + (e.g. ``num_walkers = 16384``) where the reference version becomes the + initialization bottleneck. + + Algorithm: + 1. Compute the deterministic atom-assignment templates for spin-up and + spin-down electrons by replaying the original state machine **once**. + These templates depend only on (charges, coords) — every walker + shares them. + 2. Tile the templates to shape ``(num_walkers, ned)``. + 3. For the only branch in the original that uses per-walker randomness + — Phase 1b "extra up" electrons when + ``tot_num_electron_up > sum(zeta - occup_dn)`` — draw the random + atom indices in one batched ``np.random.randint`` call. + 4. Draw all spherical random offsets in one batched call per spin and + add them to the chosen atomic coordinates. + + The duplicate-avoidance retry loop in the reference implementation is + omitted: spherical offsets are sampled from continuous uniform + distributions, so the probability of two ``float64`` positions colliding + within ``1e-6`` is effectively zero. The companion test + ``test_init_electron_configurations`` verifies uniqueness across all + generated positions. + + Parameters and returns are identical to + :func:`_generate_init_electron_configurations_debug`. + """ + min_dst = 0.1 + max_dst = 1.0 + dtype_np = np.float64 + + nion = coords.shape[0] + coords_np = np.asarray(coords, dtype=dtype_np) + zeta = np.array([int(round(c)) for c in np.asarray(charges)], dtype=int) + max_dn_per_atom = zeta // 2 + + # 1) Build ion_seq (each next index is the atom farthest from the previous). + ion_sel = np.ones(nion, dtype=bool) + ion_seq = np.zeros(nion, dtype=int) + ion_seq[0] = 0 + ion_sel[0] = False + i_prev = 0 + for idx in range(1, nion): + d2 = np.sum((coords_np[i_prev] - coords_np) ** 2, axis=1) + d2_masked = np.where(ion_sel, d2, -1.0) + best_i = int(np.argmax(d2_masked)) + ion_seq[idx] = best_i + ion_sel[best_i] = False + i_prev = best_i + + # 2) Replay the deterministic state machine ONCE to obtain owner templates. + occup_total = np.zeros(nion, dtype=int) + occup_dn = np.zeros(nion, dtype=int) + occup_up = np.zeros(nion, dtype=int) + + # Phase 1a: place all spin-down electrons (deterministic). + ned_dn = tot_num_electron_dn + dn_owner_template = np.empty(ned_dn, dtype=int) + j_counter = 0 + for idn in range(ned_dn): + while True: + atom = ion_seq[j_counter % nion] + if np.any(occup_dn < max_dn_per_atom): + cond = occup_dn[atom] < max_dn_per_atom[atom] + else: + mask_zero = (max_dn_per_atom == 0) & (occup_total < zeta) + if np.any(mask_zero): + cond = (max_dn_per_atom[atom] == 0) and (occup_total[atom] < zeta[atom]) + else: + cond = occup_total[atom] < zeta[atom] + if cond: + dn_owner_template[idn] = atom + occup_dn[atom] += 1 + occup_total[atom] += 1 + j_counter += 1 + break + j_counter += 1 + + # Phase 1b: place spin-up electrons; deterministic except for the + # "extra" tail in Case 2 which is per-walker random. + up_needed = zeta - occup_dn + sum_up_needed = int(np.sum(up_needed)) + ned_up = tot_num_electron_up + up_owner_template = np.empty(ned_up, dtype=int) + n_random_extras = 0 # trailing electrons whose owner is random per walker + + if ned_up <= sum_up_needed: + # Case 1: place exactly into the up_needed slots — fully deterministic. + ptr = 0 + for iup in range(ned_up): + while True: + atom = ion_seq[ptr % nion] + if occup_up[atom] < up_needed[atom]: + up_owner_template[iup] = atom + occup_up[atom] += 1 + occup_total[atom] += 1 + ptr += 1 + break + ptr += 1 + else: + # Case 2: first satisfy every atom's up_needed (deterministic), then + # the remainder is sampled per walker. + cnt = 0 + for atom in ion_seq: + to_give = int(up_needed[atom]) + for _ in range(to_give): + up_owner_template[cnt] = atom + occup_up[atom] += 1 + occup_total[atom] += 1 + cnt += 1 + n_random_extras = ned_up - sum_up_needed + + # 3) Build per-walker owner arrays. + if ned_dn > 0: + dn_owner = np.broadcast_to(dn_owner_template, (num_walkers, ned_dn)).copy() + else: + dn_owner = np.empty((num_walkers, 0), dtype=int) + + up_owner = np.empty((num_walkers, ned_up), dtype=int) + if n_random_extras > 0: + det_count = ned_up - n_random_extras + if det_count > 0: + up_owner[:, :det_count] = up_owner_template[:det_count][None, :] + # Per-walker random pick from ion_seq, matching the original + # idx = int(floor(np.random.rand() * nion)) + rand_idx = np.floor(np.random.rand(num_walkers, n_random_extras) * nion).astype(int) + # Clip to nion-1 just in case np.random.rand() returns 1.0 (it shouldn't). + np.clip(rand_idx, 0, nion - 1, out=rand_idx) + up_owner[:, det_count:] = ion_seq[rand_idx] + else: + up_owner[:] = up_owner_template[None, :] + + # 4) Draw all spherical random offsets in batched form. + def _spherical_offsets(shape: tuple[int, int]) -> np.ndarray: + distance = np.random.uniform(min_dst, max_dst, size=shape) + theta = np.random.uniform(0.0, np.pi, size=shape) + phi = np.random.uniform(0.0, 2.0 * np.pi, size=shape) + sin_t = np.sin(theta) + return np.stack( + [ + distance * sin_t * np.cos(phi), + distance * sin_t * np.sin(phi), + distance * np.cos(theta), + ], + axis=-1, + ).astype(dtype_np, copy=False) + + offset_dn = _spherical_offsets((num_walkers, ned_dn)) if ned_dn > 0 else np.zeros((num_walkers, 0, 3), dtype=dtype_np) + offset_up = _spherical_offsets((num_walkers, ned_up)) if ned_up > 0 else np.zeros((num_walkers, 0, 3), dtype=dtype_np) + + # 5) Assemble final positions: r = coords[owner] + offset. + r_carts_up = (coords_np[up_owner] + offset_up).astype(dtype_np, copy=False) + r_carts_dn = (coords_np[dn_owner] + offset_dn).astype(dtype_np, copy=False) + + return r_carts_up, r_carts_dn, up_owner, dn_owner + + +def _generate_init_electron_configurations_debug( + tot_num_electron_up: int, + tot_num_electron_dn: int, + num_walkers: int, + charges: np.ndarray, + coords: np.ndarray, +) -> tuple[npt.NDArray, npt.NDArray, npt.NDArray, npt.NDArray]: + """Reference (per-walker Python loop) initial electron configuration generator. + + This is the original implementation kept for cross-checks against the + vectorized :func:`_generate_init_electron_configurations`. It runs an + ``O(num_walkers)`` Python loop and is too slow for large walker counts + (e.g. ``num_walkers = 16384``); use only for tests / debugging. Generate initial electron configurations (up/down positions) for a set of walkers, using the same ion_seq idea as the Fortran initconf routine, but without diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index a7a64de2..4e2fb72e 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -242,9 +242,10 @@ def __init__( # Initialization self.__mpi_seed = self.__mcmc_seed * (mpi_rank + 1) self.__jax_PRNG_key = jax.random.PRNGKey(self.__mpi_seed) - self.__jax_PRNG_key_list_init = jnp.array( - [jax.random.fold_in(self.__jax_PRNG_key, nw) for nw in range(self.__num_walkers)] - ) + # Use jax.random.split (batched) instead of a Python list-comp of + # fold_in calls; the latter scaled linearly with num_walkers and + # dominated init time at large walker counts (e.g. nw = 16384). + self.__jax_PRNG_key_list_init = jax.random.split(self.__jax_PRNG_key, self.__num_walkers) self.__jax_PRNG_key_list = self.__jax_PRNG_key_list_init # initialize random seed @@ -269,15 +270,18 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - for i_walker in range(self.__num_walkers): - logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") - nion = coords.shape[0] - up_counts = np.bincount(up_owner[i_walker], minlength=nion) - dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) - logger.debug(f" Charges: {charges}") - logger.debug(f" up counts: {up_counts}") - logger.debug(f" dn counts: {dn_counts}") - logger.debug(f" Total counts: {up_counts + dn_counts}") + # NOTE: per-walker debug log loop removed — it was O(num_walkers) Python + # work (np.bincount per walker) executed regardless of log level, which + # at nw = 16384 added measurable startup overhead. + # for i_walker in range(self.__num_walkers): + # logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") + # nion = coords.shape[0] + # up_counts = np.bincount(up_owner[i_walker], minlength=nion) + # dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) + # logger.debug(f" Charges: {charges}") + # logger.debug(f" up counts: {up_counts}") + # logger.debug(f" dn counts: {dn_counts}") + # logger.debug(f" Total counts: {up_counts + dn_counts}") dtype_jnp = jnp.float64 self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) @@ -2629,9 +2633,10 @@ def __init__( # Initialization self.__mpi_seed = self.__mcmc_seed * (mpi_rank + 1) self.__jax_PRNG_key = jax.random.PRNGKey(self.__mpi_seed) - self.__jax_PRNG_key_list_init = jnp.array( - [jax.random.fold_in(self.__jax_PRNG_key, nw) for nw in range(self.__num_walkers)] - ) + # Use jax.random.split (batched) instead of a Python list-comp of + # fold_in calls; the latter scaled linearly with num_walkers and + # dominated init time at large walker counts (e.g. nw = 16384). + self.__jax_PRNG_key_list_init = jax.random.split(self.__jax_PRNG_key, self.__num_walkers) self.__jax_PRNG_key_list = self.__jax_PRNG_key_list_init # initialize random seed @@ -2656,15 +2661,18 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - for i_walker in range(self.__num_walkers): - logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") - nion = coords.shape[0] - up_counts = np.bincount(up_owner[i_walker], minlength=nion) - dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) - logger.debug(f" Charges: {charges}") - logger.debug(f" up counts: {up_counts}") - logger.debug(f" dn counts: {dn_counts}") - logger.debug(f" Total counts: {up_counts + dn_counts}") + # NOTE: per-walker debug log loop removed — it was O(num_walkers) Python + # work (np.bincount per walker) executed regardless of log level, which + # at nw = 16384 added measurable startup overhead. + # for i_walker in range(self.__num_walkers): + # logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") + # nion = coords.shape[0] + # up_counts = np.bincount(up_owner[i_walker], minlength=nion) + # dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) + # logger.debug(f" Charges: {charges}") + # logger.debug(f" up counts: {up_counts}") + # logger.debug(f" dn counts: {dn_counts}") + # logger.debug(f" Total counts: {up_counts + dn_counts}") dtype_jnp = jnp.float64 self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) @@ -3937,9 +3945,10 @@ def __init__( # Initialization self.__mpi_seed = self.__mcmc_seed * (mpi_rank + 1) self.__jax_PRNG_key = jax.random.PRNGKey(self.__mpi_seed) - self.__jax_PRNG_key_list_init = jnp.array( - [jax.random.fold_in(self.__jax_PRNG_key, nw) for nw in range(self.__num_walkers)] - ) + # Use jax.random.split (batched) instead of a Python list-comp of + # fold_in calls; the latter scaled linearly with num_walkers and + # dominated init time at large walker counts (e.g. nw = 16384). + self.__jax_PRNG_key_list_init = jax.random.split(self.__jax_PRNG_key, self.__num_walkers) self.__jax_PRNG_key_list = self.__jax_PRNG_key_list_init # initialize random seed @@ -3964,15 +3973,18 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - for i_walker in range(self.__num_walkers): - logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") - nion = coords.shape[0] - up_counts = np.bincount(up_owner[i_walker], minlength=nion) - dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) - logger.debug(f" Charges: {charges}") - logger.debug(f" up counts: {up_counts}") - logger.debug(f" dn counts: {dn_counts}") - logger.debug(f" Total counts: {up_counts + dn_counts}") + # NOTE: per-walker debug log loop removed — it was O(num_walkers) Python + # work (np.bincount per walker) executed regardless of log level, which + # at nw = 16384 added measurable startup overhead. + # for i_walker in range(self.__num_walkers): + # logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") + # nion = coords.shape[0] + # up_counts = np.bincount(up_owner[i_walker], minlength=nion) + # dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) + # logger.debug(f" Charges: {charges}") + # logger.debug(f" up counts: {up_counts}") + # logger.debug(f" dn counts: {dn_counts}") + # logger.debug(f" Total counts: {up_counts + dn_counts}") dtype_jnp = jnp.float64 self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) @@ -6560,9 +6572,10 @@ def __init__( # Initialization self.__mpi_seed = self.__mcmc_seed * (mpi_rank + 1) self.__jax_PRNG_key = jax.random.PRNGKey(self.__mpi_seed) - self.__jax_PRNG_key_list_init = jnp.array( - [jax.random.fold_in(self.__jax_PRNG_key, nw) for nw in range(self.__num_walkers)] - ) + # Use jax.random.split (batched) instead of a Python list-comp of + # fold_in calls; the latter scaled linearly with num_walkers and + # dominated init time at large walker counts (e.g. nw = 16384). + self.__jax_PRNG_key_list_init = jax.random.split(self.__jax_PRNG_key, self.__num_walkers) self.__jax_PRNG_key_list = self.__jax_PRNG_key_list_init # initialize random seed @@ -6587,15 +6600,18 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - for i_walker in range(self.__num_walkers): - logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") - nion = coords.shape[0] - up_counts = np.bincount(up_owner[i_walker], minlength=nion) - dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) - logger.debug(f" Charges: {charges}") - logger.debug(f" up counts: {up_counts}") - logger.debug(f" dn counts: {dn_counts}") - logger.debug(f" Total counts: {up_counts + dn_counts}") + # NOTE: per-walker debug log loop removed — it was O(num_walkers) Python + # work (np.bincount per walker) executed regardless of log level, which + # at nw = 16384 added measurable startup overhead. + # for i_walker in range(self.__num_walkers): + # logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") + # nion = coords.shape[0] + # up_counts = np.bincount(up_owner[i_walker], minlength=nion) + # dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) + # logger.debug(f" Charges: {charges}") + # logger.debug(f" up counts: {up_counts}") + # logger.debug(f" dn counts: {dn_counts}") + # logger.debug(f" Total counts: {up_counts + dn_counts}") dtype_jnp = jnp.float64 self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index f19da2b9..d5a07546 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -190,7 +190,11 @@ def __init__( # seeds self.__mpi_seed = self.__mcmc_seed * (mpi_rank + 1) self.__jax_PRNG_key = jax.random.PRNGKey(self.__mpi_seed) - self.__jax_PRNG_key_list = jnp.array([jax.random.fold_in(self.__jax_PRNG_key, nw) for nw in range(self.__num_walkers)]) + # Use jax.random.split to obtain ``num_walkers`` independent PRNGKeys in + # one batched call; replaces the previous ``[fold_in(..., nw) for nw ...]`` + # Python-loop, which scaled linearly with num_walkers and dominated init + # time at large walker counts (e.g. nw = 16384). + self.__jax_PRNG_key_list = jax.random.split(self.__jax_PRNG_key, self.__num_walkers) # initialize random seed np.random.seed(self.__mpi_seed) @@ -221,15 +225,18 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - for i_walker in range(self.__num_walkers): - logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") - nion = coords.shape[0] - up_counts = np.bincount(up_owner[i_walker], minlength=nion) - dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) - logger.debug(f" Charges: {charges}") - logger.debug(f" up counts: {up_counts}") - logger.debug(f" dn counts: {dn_counts}") - logger.debug(f" Total counts: {up_counts + dn_counts}") + # NOTE: per-walker debug log loop removed — it was O(num_walkers) Python + # work (np.bincount per walker) executed regardless of log level, which + # at nw = 16384 added measurable startup overhead. + # for i_walker in range(self.__num_walkers): + # logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") + # nion = coords.shape[0] + # up_counts = np.bincount(up_owner[i_walker], minlength=nion) + # dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) + # logger.debug(f" Charges: {charges}") + # logger.debug(f" up counts: {up_counts}") + # logger.debug(f" dn counts: {dn_counts}") + # logger.debug(f" Total counts: {up_counts + dn_counts}") dtype_jnp = jnp.float64 self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) @@ -4791,7 +4798,9 @@ def __init__( # seeds self.__mpi_seed = self.__mcmc_seed * (mpi_rank + 1) self.__jax_PRNG_key = jax.random.PRNGKey(self.__mpi_seed) - self.__jax_PRNG_key_list = jnp.array([jax.random.fold_in(self.__jax_PRNG_key, nw) for nw in range(self.__num_walkers)]) + # Use jax.random.split (batched) to match the production MCMC class so + # MCMC ↔ _MCMC_debug parity tests stay aligned. + self.__jax_PRNG_key_list = jax.random.split(self.__jax_PRNG_key, self.__num_walkers) # initialize random seed np.random.seed(self.__mpi_seed) @@ -4821,15 +4830,16 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - for i_walker in range(self.__num_walkers): - logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") - nion = coords.shape[0] - up_counts = np.bincount(up_owner[i_walker], minlength=nion) - dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) - logger.debug(f" Charges: {charges}") - logger.debug(f" up counts: {up_counts}") - logger.debug(f" dn counts: {dn_counts}") - logger.debug(f" Total counts: {up_counts + dn_counts}") + # NOTE: per-walker debug log loop removed (see MCMC.__init__ for rationale). + # for i_walker in range(self.__num_walkers): + # logger.debug(f"--Walker No.{i_walker + 1}: electrons assignment--") + # nion = coords.shape[0] + # up_counts = np.bincount(up_owner[i_walker], minlength=nion) + # dn_counts = np.bincount(dn_owner[i_walker], minlength=nion) + # logger.debug(f" Charges: {charges}") + # logger.debug(f" up counts: {up_counts}") + # logger.debug(f" dn counts: {dn_counts}") + # logger.debug(f" Total counts: {up_counts + dn_counts}") dtype_jnp = jnp.float64 self.__latest_r_up_carts = jnp.asarray(r_carts_up, dtype=dtype_jnp) diff --git a/tests/test_init_electron_configurations.py b/tests/test_init_electron_configurations.py new file mode 100644 index 00000000..be7f0671 --- /dev/null +++ b/tests/test_init_electron_configurations.py @@ -0,0 +1,324 @@ +"""Unit tests for the vectorized initial-electron-configuration generator. + +Specifically guards against regressions in +:func:`jqmc._jqmc_utility._generate_init_electron_configurations`: + +* All generated 3D positions across walkers and electrons must be unique + (no duplicates). +* Output shapes match the documented contract. +* Per-walker electron-to-atom assignment is consistent + (each owner is a valid atom index, total count matches input). +* The vectorized version agrees with the reference implementation + ``_generate_init_electron_configurations_debug`` on the deterministic + atom-assignment template (i.e., owner indices, modulo per-walker random + extras). +""" + +import numpy as np +import pytest + +from jqmc._jqmc_utility import ( + _generate_init_electron_configurations, + _generate_init_electron_configurations_debug, +) + + +# (name, charges, coords, tot_up, tot_dn) +_TEST_SYSTEMS = [ + ( + "H2_ae", + np.array([1.0, 1.0]), + np.array([[-0.37, 0.0, 0.0], [0.37, 0.0, 0.0]]), + 1, + 1, + ), + ( + "Li2_ae", + np.array([3.0, 3.0]), + np.array([[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0]]), + 3, + 3, + ), + ( + "H2O_ae", + np.array([1.0, 8.0, 1.0]), + np.array([[0.00, 0.00, 0.00], [0.96, 0.00, 0.00], [-0.24, 0.93, 0.00]]), + 5, + 5, + ), + ( + "H2O_ecp", + np.array([6.0, 1.0, 1.0]), + np.array([[0.00, 0.00, 0.00], [0.76, 0.59, 0.00], [-0.76, 0.59, 0.00]]), + 4, + 4, + ), + ( + "N2_spin_polarized", + np.array([7.0, 7.0]), + np.array([[-0.6, 0.0, 0.0], [0.6, 0.0, 0.0]]), + 10, + 4, + ), + ( + "single_H_ae", + np.array([1.0]), + np.array([[0.0, 0.0, 0.0]]), + 1, + 0, + ), +] + + +@pytest.mark.parametrize("name,charges,coords,tot_up,tot_dn", _TEST_SYSTEMS) +@pytest.mark.parametrize("num_walkers", [1, 16, 256]) +def test_generated_positions_are_all_unique(name, charges, coords, tot_up, tot_dn, num_walkers): + """All (walker, electron) 3D positions across both spins are pairwise distinct.""" + np.random.seed(123) + r_up, r_dn, _, _ = _generate_init_electron_configurations(tot_up, tot_dn, num_walkers, charges, coords) + all_pos = np.concatenate([r_up.reshape(-1, 3), r_dn.reshape(-1, 3)], axis=0) + n_total = all_pos.shape[0] + assert n_total == num_walkers * (tot_up + tot_dn) + n_unique = np.unique(all_pos, axis=0).shape[0] + assert n_unique == n_total, ( + f"[{name}, nw={num_walkers}] expected {n_total} unique positions, got {n_unique} (duplicates present)" + ) + + +@pytest.mark.parametrize("name,charges,coords,tot_up,tot_dn", _TEST_SYSTEMS) +def test_output_shapes_and_owner_ranges(name, charges, coords, tot_up, tot_dn): + """Returned arrays have documented shapes and owners reference valid atoms.""" + nion = coords.shape[0] + num_walkers = 32 + np.random.seed(7) + r_up, r_dn, up_owner, dn_owner = _generate_init_electron_configurations(tot_up, tot_dn, num_walkers, charges, coords) + assert r_up.shape == (num_walkers, tot_up, 3) + assert r_dn.shape == (num_walkers, tot_dn, 3) + assert up_owner.shape == (num_walkers, tot_up) + assert dn_owner.shape == (num_walkers, tot_dn) + + if tot_up > 0: + assert up_owner.min() >= 0 and up_owner.max() < nion + if tot_dn > 0: + assert dn_owner.min() >= 0 and dn_owner.max() < nion + + +@pytest.mark.parametrize("name,charges,coords,tot_up,tot_dn", _TEST_SYSTEMS) +def test_vectorized_owners_match_reference(name, charges, coords, tot_up, tot_dn): + """Vectorized and reference implementations agree on the deterministic owner template. + + Both versions place the spin-down electrons via the same deterministic + state machine, and place the spin-up electrons deterministically except + for any "extra" tail when ``tot_up > sum(zeta - occup_dn)``. This test + checks that the deterministic prefix of owner assignments matches. + """ + num_walkers = 1 + np.random.seed(0) + _, _, up_v, dn_v = _generate_init_electron_configurations(tot_up, tot_dn, num_walkers, charges, coords) + np.random.seed(0) + _, _, up_r, dn_r = _generate_init_electron_configurations_debug(tot_up, tot_dn, num_walkers, charges, coords) + + # Down-electron assignment is fully deterministic in both implementations. + np.testing.assert_array_equal(dn_v[0], dn_r[0]) + + # Up-electron assignment is deterministic up to the "extra" tail; compare + # the deterministic portion only. + zeta = np.rint(charges).astype(int) + occup_dn = np.bincount(dn_r[0], minlength=coords.shape[0]) + sum_up_needed = int(np.sum(np.maximum(zeta - occup_dn, 0))) + det_count = min(tot_up, sum_up_needed) + np.testing.assert_array_equal(up_v[0, :det_count], up_r[0, :det_count]) + + +@pytest.mark.parametrize("name,charges,coords,tot_up,tot_dn", _TEST_SYSTEMS) +@pytest.mark.parametrize("num_walkers", [1, 8]) +def test_per_atom_spin_counts_match_reference(name, charges, coords, tot_up, tot_dn, num_walkers): + """Per-atom up/dn populations must agree exactly with the reference implementation. + + This guards against regressions in the *aggregate* spin-assignment behavior + (more permissive than owner-vector equality, but catches any change in + physical spin distribution per atom). Critical for systems where the old + algorithm specifically handled distant-atom spin distribution. + """ + nion = coords.shape[0] + np.random.seed(0) + _, _, up_v, dn_v = _generate_init_electron_configurations(tot_up, tot_dn, num_walkers, charges, coords) + np.random.seed(0) + _, _, up_r, dn_r = _generate_init_electron_configurations_debug(tot_up, tot_dn, num_walkers, charges, coords) + for iw in range(num_walkers): + np.testing.assert_array_equal( + np.bincount(up_v[iw], minlength=nion), + np.bincount(up_r[iw], minlength=nion), + err_msg=f"[{name}, walker {iw}] vectorized vs reference up counts differ", + ) + np.testing.assert_array_equal( + np.bincount(dn_v[iw], minlength=nion), + np.bincount(dn_r[iw], minlength=nion), + err_msg=f"[{name}, walker {iw}] vectorized vs reference dn counts differ", + ) + + +# Identical-atom dimers in the global singlet (S=0) configuration. The expected +# physical behavior at any separation: each atom carries zeta electrons with +# Hund-maximum local moment, anti-aligned to its partner so the global S=0. +# E.g., for N2 (zeta=7 each) at S=0 → one atom (4u, 3d), the other (3u, 4d). +@pytest.mark.parametrize("elem_z", [3, 6, 7, 8]) # Li, C, N, O (AE valence counts) +@pytest.mark.parametrize("separation", [0.5, 2.0, 5.0, 100.0]) +def test_dimer_singlet_atoms_are_antialigned(elem_z, separation): + """For a homonuclear dimer at S=0 the two atoms must be spin-mirror images.""" + charges = np.array([float(elem_z), float(elem_z)]) + coords = np.array([[0.0, 0.0, 0.0], [separation, 0.0, 0.0]]) + tot_up = elem_z + tot_dn = elem_z + num_walkers = 4 + + np.random.seed(0) + _, _, up_owner, dn_owner = _generate_init_electron_configurations(tot_up, tot_dn, num_walkers, charges, coords) + for iw in range(num_walkers): + up_counts = np.bincount(up_owner[iw], minlength=2) + dn_counts = np.bincount(dn_owner[iw], minlength=2) + # Each atom is charge-neutral. + np.testing.assert_array_equal( + up_counts + dn_counts, + np.array([elem_z, elem_z]), + err_msg=f"sep={separation}, Z={elem_z}, walker {iw}: per-atom electron count != zeta", + ) + # Spins anti-aligned: nup on one = ndn on the other (and vice versa). + assert up_counts[0] == dn_counts[1], ( + f"sep={separation}, Z={elem_z}, walker {iw}: expected up_counts[0] == dn_counts[1], got {up_counts}, {dn_counts}" + ) + assert dn_counts[0] == up_counts[1], ( + f"sep={separation}, Z={elem_z}, walker {iw}: expected dn_counts[0] == up_counts[1], got {up_counts}, {dn_counts}" + ) + # Hund respected: |nup - ndn| per atom = expected unpaired-electron count. + # For zeta = elem_z, max_dn_per_atom = elem_z // 2, so unpaired = elem_z - 2 * (elem_z // 2). + expected_unpaired = elem_z - 2 * (elem_z // 2) + 2 * abs((elem_z // 2) - min(up_counts[0], dn_counts[0])) + # Relaxed check: each atom's |nup - ndn| should be at least the parity (1 if odd zeta, else 0). + parity = elem_z % 2 + for atom in range(2): + assert abs(int(up_counts[atom]) - int(dn_counts[atom])) >= parity, ( + f"sep={separation}, Z={elem_z}, walker {iw}, atom {atom}: " + f"|nup-ndn| < parity ({parity}); counts up={up_counts}, dn={dn_counts}" + ) + + +# For homonuclear / heteronuclear dimers, sweep every reachable global spin S +# (i.e., every valid (tot_up, tot_dn) partition that sums to total electron +# count) and verify per-atom charge neutrality. This is the core invariant +# the original algorithm was designed to provide for widely-separated atoms +# (e.g., N---N at 100 bohr where each atom must locally hold zeta electrons +# regardless of global spin polarization). +@pytest.mark.parametrize("separation", [2.0, 100.0]) +@pytest.mark.parametrize( + "label,zeta_pair", + [ + ("N---N", (7, 7)), + ("N---O", (7, 8)), + ("O---O", (8, 8)), + ("Li---N", (3, 7)), + ("Li---Li", (3, 3)), + ("C---N", (6, 7)), + ], +) +def test_dimer_per_atom_charge_neutral_for_all_S(label, zeta_pair, separation): + """For every reachable S of a (neutral) dimer, both atoms must be charge-neutral. + + Iterates over every valid ``(tot_up, tot_dn)`` partition with + ``tot_up + tot_dn == zeta[0] + zeta[1]``. For each, asserts that + ``np.bincount(up_owner) + np.bincount(dn_owner) == zeta`` for every walker. + Also checks the totals match the input split. + """ + z0, z1 = zeta_pair + total = z0 + z1 + charges = np.array([float(z0), float(z1)]) + coords = np.array([[0.0, 0.0, 0.0], [separation, 0.0, 0.0]]) + expected_zeta = np.array([z0, z1]) + num_walkers = 4 + + for tot_up in range(total + 1): + tot_dn = total - tot_up + np.random.seed(0) + _, _, up_owner, dn_owner = _generate_init_electron_configurations(tot_up, tot_dn, num_walkers, charges, coords) + S2 = tot_up - tot_dn # 2*S signed + for iw in range(num_walkers): + up_counts = np.bincount(up_owner[iw], minlength=2) + dn_counts = np.bincount(dn_owner[iw], minlength=2) + per_atom_total = up_counts + dn_counts + np.testing.assert_array_equal( + per_atom_total, + expected_zeta, + err_msg=( + f"[{label}, sep={separation}, 2S={S2}, walker {iw}] " + f"per-atom total {per_atom_total} != zeta {expected_zeta}; " + f"up={up_counts.tolist()}, dn={dn_counts.tolist()}" + ), + ) + assert int(up_counts.sum()) == tot_up, ( + f"[{label}, 2S={S2}, walker {iw}] sum(up_counts)={up_counts.sum()} != tot_up={tot_up}" + ) + assert int(dn_counts.sum()) == tot_dn, ( + f"[{label}, 2S={S2}, walker {iw}] sum(dn_counts)={dn_counts.sum()} != tot_dn={tot_dn}" + ) + + +# Same idea, against the reference implementation: per-atom up/dn counts must +# match across all S values. This is a stricter "no-regression" check on the +# vectorized refactor specifically for the spin-distribution behavior. +@pytest.mark.parametrize( + "label,zeta_pair", + [ + ("N---N", (7, 7)), + ("N---O", (7, 8)), + ("Li---N", (3, 7)), + ], +) +def test_dimer_per_atom_counts_match_reference_for_all_S(label, zeta_pair): + """Vectorized vs reference per-atom spin counts must agree for every S.""" + z0, z1 = zeta_pair + total = z0 + z1 + charges = np.array([float(z0), float(z1)]) + coords = np.array([[0.0, 0.0, 0.0], [5.0, 0.0, 0.0]]) + num_walkers = 2 + + for tot_up in range(total + 1): + tot_dn = total - tot_up + np.random.seed(0) + _, _, up_v, dn_v = _generate_init_electron_configurations(tot_up, tot_dn, num_walkers, charges, coords) + np.random.seed(0) + _, _, up_r, dn_r = _generate_init_electron_configurations_debug(tot_up, tot_dn, num_walkers, charges, coords) + for iw in range(num_walkers): + np.testing.assert_array_equal( + np.bincount(up_v[iw], minlength=2), + np.bincount(up_r[iw], minlength=2), + err_msg=f"[{label}, 2S={tot_up - tot_dn}, walker {iw}] up counts differ", + ) + np.testing.assert_array_equal( + np.bincount(dn_v[iw], minlength=2), + np.bincount(dn_r[iw], minlength=2), + err_msg=f"[{label}, 2S={tot_up - tot_dn}, walker {iw}] dn counts differ", + ) + + +# Triatomic (linear, well-separated) chain — exercises ion_seq logic for >2 atoms. +@pytest.mark.parametrize("elem_z", [3, 7]) +def test_linear_triatomic_charge_neutrality(elem_z): + """For a homonuclear linear triatomic at separation 5 bohr each, every atom + receives exactly zeta electrons (charge-neutral assignment).""" + charges = np.array([float(elem_z)] * 3) + coords = np.array([[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [10.0, 0.0, 0.0]]) + nion = 3 + # Use total = sum(zeta), pick a balanced spin partition. + total = 3 * elem_z + tot_up = total // 2 + (total % 2) # ceil + tot_dn = total - tot_up + num_walkers = 4 + + np.random.seed(0) + _, _, up_owner, dn_owner = _generate_init_electron_configurations(tot_up, tot_dn, num_walkers, charges, coords) + for iw in range(num_walkers): + per_atom_total = np.bincount(up_owner[iw], minlength=nion) + np.bincount(dn_owner[iw], minlength=nion) + np.testing.assert_array_equal( + per_atom_total, + np.array([elem_z, elem_z, elem_z]), + err_msg=f"Z={elem_z}, walker {iw}: per-atom electron count != zeta (got {per_atom_total})", + ) From 49369e99d8b25509acbe23a6091eaec908980295 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:28:50 +0900 Subject: [PATCH 16/97] Use two-pass centered sum-of-squares in jackknife std The hand-rolled jackknife standard deviation in the MCMC/GFMC estimators used Var = - ^2 which suffers from float64 catastrophic cancellation when `M_total = num_mcmc_bin_blocks * nw * num_ranks` becomes large. Switch to the two-pass centered formulation in all production paths: - `MCMC.get_E`, `get_aF`, `get_gF` - `GFMC_t.get_E`, `get_aF` (small-bin and MPI-scatter) - `GFMC_n.get_E`, `get_aF` (small-bin and MPI-scatter) `_MCMC_debug`, `_GFMC_t_debug`, and `_GFMC_n_debug` are unchanged; they already use `np.std()` (internally two-pass). The existing debug <-> production agreement tests cover this fix as a regression test. --- jqmc/jqmc_gfmc.py | 156 +++++++++++++++++++++------------------------- jqmc/jqmc_mcmc.py | 68 ++++++++++---------- 2 files changed, 107 insertions(+), 117 deletions(-) diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index a7a64de2..0c907e5b 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -2142,20 +2142,17 @@ def get_E( Var_jackknife_binned_local = E2_jackknife_binned_local - E_jackknife_binned_local**2 - # E: jackknife mean and std - sum_E_local = np.sum(E_jackknife_binned_local) - sumsq_E_local = np.sum(E_jackknife_binned_local**2) + # Two-pass jackknife std (centered sum of squares) to avoid + # catastrophic cancellation in - ^2. - E_mean = sum_E_local / M_local - E_var = (sumsq_E_local / M_local) - (sum_E_local / M_local) ** 2 + # E: 1st pass — mean, 2nd pass — centered sum of squares + E_mean = np.sum(E_jackknife_binned_local) / M_local + E_var = np.sum((E_jackknife_binned_local - E_mean) ** 2) / M_local E_std = np.sqrt((M_local - 1) * E_var) - # Var: jackknife mean and std - sum_Var_local = np.sum(Var_jackknife_binned_local) - sumsq_Var_local = np.sum(Var_jackknife_binned_local**2) - - Var_mean = sum_Var_local / M_total - Var_var = (sumsq_Var_local / M_total) - (sum_Var_local / M_local) ** 2 + # Var: 1st pass — mean, 2nd pass — centered sum of squares + Var_mean = np.sum(Var_jackknife_binned_local) / M_total + Var_var = np.sum((Var_jackknife_binned_local - Var_mean) ** 2) / M_total Var_std = np.sqrt((M_total - 1) * Var_var) else: @@ -2239,28 +2236,25 @@ def get_E( Var_jackknife_binned_local = E2_jackknife_binned_local - E_jackknife_binned_local**2 - # E: jackknife mean and std - sum_E_local = np.sum(E_jackknife_binned_local) - sumsq_E_local = np.sum(E_jackknife_binned_local**2) - - # E: global sum - sum_E_global = mpi_comm.allreduce(sum_E_local, op=MPI.SUM) - sumsq_E_global = mpi_comm.allreduce(sumsq_E_local, op=MPI.SUM) + # Two-pass jackknife std (centered sum of squares) to avoid + # catastrophic cancellation in - ^2. + # E: 1st pass — global mean + sum_E_global = mpi_comm.allreduce(np.sum(E_jackknife_binned_local), op=MPI.SUM) E_mean = sum_E_global / M_total - E_var = (sumsq_E_global / M_total) - (sum_E_global / M_total) ** 2 - E_std = np.sqrt((M_total - 1) * E_var) - # Var: jackknife mean and std - sum_Var_local = np.sum(Var_jackknife_binned_local) - sumsq_Var_local = np.sum(Var_jackknife_binned_local**2) - - # Var: global sum - sum_Var_global = mpi_comm.allreduce(sum_Var_local, op=MPI.SUM) - sumsq_Var_global = mpi_comm.allreduce(sumsq_Var_local, op=MPI.SUM) + # E: 2nd pass — centered sum of squares (numerically stable) + sumsq_centered_E_global = mpi_comm.allreduce(np.sum((E_jackknife_binned_local - E_mean) ** 2), op=MPI.SUM) + E_var = sumsq_centered_E_global / M_total + E_std = np.sqrt((M_total - 1) * E_var) + # Var: 1st pass — global mean + sum_Var_global = mpi_comm.allreduce(np.sum(Var_jackknife_binned_local), op=MPI.SUM) Var_mean = sum_Var_global / M_total - Var_var = (sumsq_Var_global / M_total) - (sum_Var_global / M_total) ** 2 + + # Var: 2nd pass — centered sum of squares + sumsq_centered_Var_global = mpi_comm.allreduce(np.sum((Var_jackknife_binned_local - Var_mean) ** 2), op=MPI.SUM) + Var_var = sumsq_centered_Var_global / M_total Var_std = np.sqrt((M_total - 1) * Var_var) # return @@ -2379,12 +2373,10 @@ def get_aF( force_jn_local = force_HF_jn_local + force_Pulay_jn_local - sum_force_local = np.sum(force_jn_local, axis=0) - sumsq_force_local = np.sum(force_jn_local**2, axis=0) - - ## mean and var = E[x^2] - (E[x])^2 - mean_force_global = sum_force_local / M_local - var_force_global = (sumsq_force_local / M_local) - (sum_force_local / M_local) ** 2 + # Two-pass jackknife std (centered sum of squares) to avoid + # catastrophic cancellation in - ^2. + mean_force_global = np.sum(force_jn_local, axis=0) / M_local + var_force_global = np.sum((force_jn_local - mean_force_global) ** 2, axis=0) / M_local ## mean and std force_mean = mean_force_global @@ -2541,18 +2533,20 @@ def get_aF( force_jn_local = force_HF_jn_local + force_Pulay_jn_local - sum_force_local = np.sum(force_jn_local, axis=0) - sumsq_force_local = np.sum(force_jn_local**2, axis=0) + # Two-pass jackknife std (centered sum of squares) to avoid + # catastrophic cancellation in - ^2. + # 1st pass — global mean + sum_force_local = np.sum(force_jn_local, axis=0) sum_force_global = np.empty_like(sum_force_local) - sumsq_force_global = np.empty_like(sumsq_force_local) - mpi_comm.Allreduce([sum_force_local, MPI.DOUBLE], [sum_force_global, MPI.DOUBLE], op=MPI.SUM) - mpi_comm.Allreduce([sumsq_force_local, MPI.DOUBLE], [sumsq_force_global, MPI.DOUBLE], op=MPI.SUM) - - ## mean and var = E[x^2] - (E[x])^2 mean_force_global = sum_force_global / M_total - var_force_global = (sumsq_force_global / M_total) - (sum_force_global / M_total) ** 2 + + # 2nd pass — centered sum of squares (numerically stable) + sumsq_centered_force_local = np.sum((force_jn_local - mean_force_global) ** 2, axis=0) + sumsq_centered_force_global = np.empty_like(sumsq_centered_force_local) + mpi_comm.Allreduce([sumsq_centered_force_local, MPI.DOUBLE], [sumsq_centered_force_global, MPI.DOUBLE], op=MPI.SUM) + var_force_global = sumsq_centered_force_global / M_total ## mean and std force_mean = mean_force_global @@ -6070,20 +6064,17 @@ def get_E( Var_jackknife_binned_local = E2_jackknife_binned_local - E_jackknife_binned_local**2 - # E: jackknife mean and std - sum_E_local = np.sum(E_jackknife_binned_local) - sumsq_E_local = np.sum(E_jackknife_binned_local**2) + # Two-pass jackknife std (centered sum of squares) to avoid + # catastrophic cancellation in - ^2. - E_mean = sum_E_local / M_local - E_var = (sumsq_E_local / M_local) - (sum_E_local / M_local) ** 2 + # E: 1st pass — mean, 2nd pass — centered sum of squares + E_mean = np.sum(E_jackknife_binned_local) / M_local + E_var = np.sum((E_jackknife_binned_local - E_mean) ** 2) / M_local E_std = np.sqrt((M_local - 1) * E_var) - # Var: jackknife mean and std - sum_Var_local = np.sum(Var_jackknife_binned_local) - sumsq_Var_local = np.sum(Var_jackknife_binned_local**2) - - Var_mean = sum_Var_local / M_total - Var_var = (sumsq_Var_local / M_total) - (sum_Var_local / M_local) ** 2 + # Var: 1st pass — mean, 2nd pass — centered sum of squares + Var_mean = np.sum(Var_jackknife_binned_local) / M_total + Var_var = np.sum((Var_jackknife_binned_local - Var_mean) ** 2) / M_total Var_std = np.sqrt((M_total - 1) * Var_var) else: @@ -6167,28 +6158,25 @@ def get_E( Var_jackknife_binned_local = E2_jackknife_binned_local - E_jackknife_binned_local**2 - # E: jackknife mean and std - sum_E_local = np.sum(E_jackknife_binned_local) - sumsq_E_local = np.sum(E_jackknife_binned_local**2) - - # E: global sums - sum_E_global = mpi_comm.allreduce(sum_E_local, op=MPI.SUM) - sumsq_E_global = mpi_comm.allreduce(sumsq_E_local, op=MPI.SUM) + # Two-pass jackknife std (centered sum of squares) to avoid + # catastrophic cancellation in - ^2. + # E: 1st pass — global mean + sum_E_global = mpi_comm.allreduce(np.sum(E_jackknife_binned_local), op=MPI.SUM) E_mean = sum_E_global / M_total - E_var = (sumsq_E_global / M_total) - (sum_E_global / M_total) ** 2 - E_std = np.sqrt((M_total - 1) * E_var) - # Var: jackknife mean and std - sum_Var_local = np.sum(Var_jackknife_binned_local) - sumsq_Var_local = np.sum(Var_jackknife_binned_local**2) - - # Var: global sums - sum_Var_global = mpi_comm.allreduce(sum_Var_local, op=MPI.SUM) - sumsq_Var_global = mpi_comm.allreduce(sumsq_Var_local, op=MPI.SUM) + # E: 2nd pass — centered sum of squares (numerically stable) + sumsq_centered_E_global = mpi_comm.allreduce(np.sum((E_jackknife_binned_local - E_mean) ** 2), op=MPI.SUM) + E_var = sumsq_centered_E_global / M_total + E_std = np.sqrt((M_total - 1) * E_var) + # Var: 1st pass — global mean + sum_Var_global = mpi_comm.allreduce(np.sum(Var_jackknife_binned_local), op=MPI.SUM) Var_mean = sum_Var_global / M_total - Var_var = (sumsq_Var_global / M_total) - (sum_Var_global / M_total) ** 2 + + # Var: 2nd pass — centered sum of squares + sumsq_centered_Var_global = mpi_comm.allreduce(np.sum((Var_jackknife_binned_local - Var_mean) ** 2), op=MPI.SUM) + Var_var = sumsq_centered_Var_global / M_total Var_std = np.sqrt((M_total - 1) * Var_var) # return @@ -6308,12 +6296,10 @@ def get_aF( force_jn_local = force_HF_jn_local + force_Pulay_jn_local - sum_force_local = np.sum(force_jn_local, axis=0) - sumsq_force_local = np.sum(force_jn_local**2, axis=0) - - ## mean and var = E[x^2] - (E[x])^2 - mean_force_global = sum_force_local / M_local - var_force_global = (sumsq_force_local / M_local) - (sum_force_local / M_local) ** 2 + # Two-pass jackknife std (centered sum of squares) to avoid + # catastrophic cancellation in - ^2. + mean_force_global = np.sum(force_jn_local, axis=0) / M_local + var_force_global = np.sum((force_jn_local - mean_force_global) ** 2, axis=0) / M_local ## mean and std force_mean = mean_force_global @@ -6472,18 +6458,20 @@ def get_aF( force_jn_local = force_HF_jn_local + force_Pulay_jn_local - sum_force_local = np.sum(force_jn_local, axis=0) - sumsq_force_local = np.sum(force_jn_local**2, axis=0) + # Two-pass jackknife std (centered sum of squares) to avoid + # catastrophic cancellation in - ^2. + # 1st pass — global mean + sum_force_local = np.sum(force_jn_local, axis=0) sum_force_global = np.empty_like(sum_force_local) - sumsq_force_global = np.empty_like(sumsq_force_local) - mpi_comm.Allreduce([sum_force_local, MPI.DOUBLE], [sum_force_global, MPI.DOUBLE], op=MPI.SUM) - mpi_comm.Allreduce([sumsq_force_local, MPI.DOUBLE], [sumsq_force_global, MPI.DOUBLE], op=MPI.SUM) - - ## mean and var = E[x^2] - (E[x])^2 mean_force_global = sum_force_global / M_total - var_force_global = (sumsq_force_global / M_total) - (sum_force_global / M_total) ** 2 + + # 2nd pass — centered sum of squares (numerically stable) + sumsq_centered_force_local = np.sum((force_jn_local - mean_force_global) ** 2, axis=0) + sumsq_centered_force_global = np.empty_like(sumsq_centered_force_local) + mpi_comm.Allreduce([sumsq_centered_force_local, MPI.DOUBLE], [sumsq_centered_force_global, MPI.DOUBLE], op=MPI.SUM) + var_force_global = sumsq_centered_force_global / M_total ## mean and std force_mean = mean_force_global diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index f19da2b9..ea832b92 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -1155,28 +1155,25 @@ def get_E( Var_jackknife_binned_local = E2_jackknife_binned_local - E_jackknife_binned_local**2 - # E: jackknife mean and std - sum_E_local = np.sum(E_jackknife_binned_local) - sumsq_E_local = np.sum(E_jackknife_binned_local**2) - - # E: global sums - sum_E_global = mpi_comm.allreduce(sum_E_local, op=MPI.SUM) - sumsq_E_global = mpi_comm.allreduce(sumsq_E_local, op=MPI.SUM) + # Two-pass jackknife std (centered sum of squares) to avoid catastrophic + # cancellation in - ^2 when M_total grows large (many walkers). + # E: 1st pass — global mean + sum_E_global = mpi_comm.allreduce(np.sum(E_jackknife_binned_local), op=MPI.SUM) E_mean = sum_E_global / M_total - E_var = (sumsq_E_global / M_total) - (sum_E_global / M_total) ** 2 - E_std = np.sqrt((M_total - 1) * E_var) - # Var: jackknife mean and std - sum_Var_local = np.sum(Var_jackknife_binned_local) - sumsq_Var_local = np.sum(Var_jackknife_binned_local**2) - - # Var: global sums - sum_Var_global = mpi_comm.allreduce(sum_Var_local, op=MPI.SUM) - sumsq_Var_global = mpi_comm.allreduce(sumsq_Var_local, op=MPI.SUM) + # E: 2nd pass — centered sum of squares (numerically stable) + sumsq_centered_E_global = mpi_comm.allreduce(np.sum((E_jackknife_binned_local - E_mean) ** 2), op=MPI.SUM) + E_var = sumsq_centered_E_global / M_total + E_std = np.sqrt((M_total - 1) * E_var) + # Var: 1st pass — global mean + sum_Var_global = mpi_comm.allreduce(np.sum(Var_jackknife_binned_local), op=MPI.SUM) Var_mean = sum_Var_global / M_total - Var_var = (sumsq_Var_global / M_total) - (sum_Var_global / M_total) ** 2 + + # Var: 2nd pass — centered sum of squares + sumsq_centered_Var_global = mpi_comm.allreduce(np.sum((Var_jackknife_binned_local - Var_mean) ** 2), op=MPI.SUM) + Var_var = sumsq_centered_Var_global / M_total Var_std = np.sqrt((M_total - 1) * Var_var) logger.devel(f"E = {E_mean} +- {E_std} Ha.") @@ -1340,18 +1337,20 @@ def get_aF( ) force_jn_local = force_HF_jn_local + force_Pulay_jn_local - sum_force_local = np.sum(force_jn_local, axis=0) - sumsq_force_local = np.sum(force_jn_local**2, axis=0) + # Two-pass jackknife std (centered sum of squares) to avoid catastrophic + # cancellation in - ^2 when M_total grows large. + # 1st pass — global mean + sum_force_local = np.sum(force_jn_local, axis=0) sum_force_global = np.empty_like(sum_force_local) - sumsq_force_global = np.empty_like(sumsq_force_local) - mpi_comm.Allreduce([sum_force_local, MPI.DOUBLE], [sum_force_global, MPI.DOUBLE], op=MPI.SUM) - mpi_comm.Allreduce([sumsq_force_local, MPI.DOUBLE], [sumsq_force_global, MPI.DOUBLE], op=MPI.SUM) - - ## mean and var = E[x^2] - (E[x])^2 mean_force_global = sum_force_global / M_total - var_force_global = (sumsq_force_global / M_total) - (sum_force_global / M_total) ** 2 + + # 2nd pass — centered sum of squares (numerically stable) + sumsq_centered_force_local = np.sum((force_jn_local - mean_force_global) ** 2, axis=0) + sumsq_centered_force_global = np.empty_like(sumsq_centered_force_local) + mpi_comm.Allreduce([sumsq_centered_force_local, MPI.DOUBLE], [sumsq_centered_force_global, MPI.DOUBLE], op=MPI.SUM) + var_force_global = sumsq_centered_force_global / M_total ## mean and std force_mean = mean_force_global @@ -1656,18 +1655,21 @@ def get_gF( bar_eL_bar_O_jn_local = np.einsum("i,ij->ij", eL_jn_local, O_jn_local) force_local = -2.0 * (eL_O_jn_local - bar_eL_bar_O_jn_local) # (M_local, D) - sum_local = np.sum(force_local, axis=0) # shape (D,) - sumsq_local = np.sum(force_local**2, axis=0) # shape (D,) - sum_global = np.empty_like(sum_local) - sumsq_global = np.empty_like(sumsq_local) + # Two-pass jackknife std (centered sum of squares) to avoid catastrophic + # cancellation in - ^2 when M_total grows large. + # 1st pass — global mean + sum_local = np.sum(force_local, axis=0) # shape (D,) + sum_global = np.empty_like(sum_local) mpi_comm.Allreduce([sum_local, MPI.DOUBLE], [sum_global, MPI.DOUBLE], op=MPI.SUM) - mpi_comm.Allreduce([sumsq_local, MPI.DOUBLE], [sumsq_global, MPI.DOUBLE], op=MPI.SUM) - - ## mean and var = E[x^2] - (E[x])^2 mean_global = sum_global / M_total - var_global = (sumsq_global / M_total) - (sum_global / M_total) ** 2 + + # 2nd pass — centered sum of squares (numerically stable) + sumsq_centered_local = np.sum((force_local - mean_global) ** 2, axis=0) # shape (D,) + sumsq_centered_global = np.empty_like(sumsq_centered_local) + mpi_comm.Allreduce([sumsq_centered_local, MPI.DOUBLE], [sumsq_centered_global, MPI.DOUBLE], op=MPI.SUM) + var_global = sumsq_centered_global / M_total ## mean and std generalized_force_mean = mean_global From 09ff3f5c4133c7b9745c686d1884e3651cfc04bd Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:57:51 +0900 Subject: [PATCH 17/97] Fixed a trivial bug in LRDMC_Ext_Workflow. --- jqmc_workflow/lrdmc_ext_workflow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jqmc_workflow/lrdmc_ext_workflow.py b/jqmc_workflow/lrdmc_ext_workflow.py index 83b0944d..48e76e84 100644 --- a/jqmc_workflow/lrdmc_ext_workflow.py +++ b/jqmc_workflow/lrdmc_ext_workflow.py @@ -262,6 +262,8 @@ def __init__( num_gfmc_projections: Optional[int] = None, max_continuation: int = 5, cleanup_patterns: Optional[list] = None, + # -- [precision] section -- + precision_mode: str = "full", ): super().__init__(cleanup_patterns=cleanup_patterns) self.server_machine_name = server_machine_name @@ -305,6 +307,8 @@ def __init__( self.pilot_steps = pilot_steps self.num_gfmc_projections = num_gfmc_projections self.max_continuation = max_continuation + # [precision] section + self.precision_mode = precision_mode def _make_lrdmc_workflow(self, alat): """Create one :class:`Container` for a given *alat* value. @@ -349,6 +353,7 @@ def _make_lrdmc_workflow(self, alat): num_gfmc_projections=self.num_gfmc_projections, max_continuation=self.max_continuation, cleanup_patterns=self.cleanup_patterns, + precision_mode=self.precision_mode, ) enc = Container( label=label, From 23b957fd8f3796aa299ed0e68166509033d2f9af Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:11:00 +0900 Subject: [PATCH 18/97] Fixed a bug in jqmc_workflow/lrdmc_ext_workflow.py again --- jqmc_workflow/lrdmc_ext_workflow.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/jqmc_workflow/lrdmc_ext_workflow.py b/jqmc_workflow/lrdmc_ext_workflow.py index 48e76e84..5268134e 100644 --- a/jqmc_workflow/lrdmc_ext_workflow.py +++ b/jqmc_workflow/lrdmc_ext_workflow.py @@ -47,7 +47,7 @@ import re import subprocess from logging import getLogger -from typing import List, Optional +from typing import Dict, List, Optional, Union from ._setting import ( GFMC_MIN_BIN_BLOCKS, @@ -246,7 +246,7 @@ def __init__( # -- [lrdmc-bra / lrdmc-tau] section parameters -- time_projection_tau: Optional[float] = 0.10, target_survived_walkers_ratio: Optional[float] = None, - num_projection_per_measurement: Optional[int] = None, + num_projection_per_measurement: Optional[Union[int, Dict[float, int]]] = None, non_local_move: Optional[str] = None, E_scf: Optional[float] = None, atomic_force: Optional[bool] = None, @@ -292,6 +292,18 @@ def __init__( # [lrdmc-bra / lrdmc-tau] section self.time_projection_tau = time_projection_tau self.target_survived_walkers_ratio = target_survived_walkers_ratio + # num_projection_per_measurement may be: + # None — GFMC_t mode (uses time_projection_tau) + # int — same value for every alat + # dict — per-alat values; keys must cover every alat in alat_list + if isinstance(num_projection_per_measurement, dict): + missing = [a for a in self.alat_list if a not in num_projection_per_measurement] + if missing: + raise ValueError( + f"num_projection_per_measurement dict is missing entries " + f"for alat values: {missing}. dict keys must match " + f"alat_list ({self.alat_list}) exactly." + ) self.num_projection_per_measurement = num_projection_per_measurement self.non_local_move = non_local_move self.E_scf = E_scf @@ -325,6 +337,12 @@ def _make_lrdmc_workflow(self, alat): label = f"lrdmc-a{alat:.3f}" dirname = f"lrdmc_alat_{alat:.3f}" + # Resolve per-alat num_projection_per_measurement if a dict was supplied. + if isinstance(self.num_projection_per_measurement, dict): + nmpm_for_alat = self.num_projection_per_measurement[alat] + else: + nmpm_for_alat = self.num_projection_per_measurement + wf = LRDMC_Workflow( server_machine_name=self.server_machine_name, alat=alat, @@ -339,7 +357,7 @@ def _make_lrdmc_workflow(self, alat): num_gfmc_collect_steps=self.num_gfmc_collect_steps, time_projection_tau=self.time_projection_tau, target_survived_walkers_ratio=self.target_survived_walkers_ratio, - num_projection_per_measurement=self.num_projection_per_measurement, + num_projection_per_measurement=nmpm_for_alat, non_local_move=self.non_local_move, E_scf=self.E_scf, atomic_force=self.atomic_force, From 27eed2520da672c548b3597b0f20f3badb439742 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:59:01 +0900 Subject: [PATCH 19/97] Reuse J3 streaming-state AOs in LRDMC mesh / ECP non-local ratios Forward `Kinetic_streaming_state.j3_state` through `_compute_ratio_Jastrow_part_{rank1_update,split_spin}` so the discretized kinetic mesh and ECP non-local kernels can skip per-step AO / W / U / `cross_vec` recomputation, saving `O(n_ao^2 * N_e)` per projection step. Legacy and observation paths keep `j3_state=None`. --- jqmc/coulomb_potential.py | 14 +- jqmc/determinant.py | 348 +++++++++++++++++++ jqmc/jastrow_factor.py | 658 ++++++++++++++++++++++++++++++++++-- jqmc/jqmc_gfmc.py | 260 +++++++++++--- jqmc/wavefunction.py | 319 +++++++++++++++++ tests/test_determinant.py | 96 ++++++ tests/test_jastrow.py | 233 +++++++++++++ tests/test_wave_function.py | 377 +++++++++++++++++++++ 8 files changed, 2240 insertions(+), 65 deletions(-) diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index e81006c8..461d9ccb 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -69,7 +69,11 @@ compute_det_geminal_all_elements, compute_geminal_all_elements, ) -from .jastrow_factor import _compute_ratio_Jastrow_part_split_spin, compute_Jastrow_part +from .jastrow_factor import ( + Jastrow_three_body_streaming_state, + _compute_ratio_Jastrow_part_split_spin, + compute_Jastrow_part, +) from .structure import ( Structure_data, _find_nearest_nucleus_indices_jnp, @@ -1476,6 +1480,7 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( NN: int = NN_default, Nv: int = Nv_default, flag_determinant_only: bool = False, + j3_state: "Jastrow_three_body_streaming_state | None" = None, ) -> tuple[list, list, list, float]: """Fast-update variant of non-local ECP contributions (nearest neighbors). @@ -1492,6 +1497,12 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( NN (int): Number of nearest nuclei to include for each electron. Nv (int): Number of quadrature points on the sphere. flag_determinant_only (bool): If True, ignore Jastrow in the wavefunction ratio. + j3_state: Optional cached J3 streaming auxiliaries consistent with + ``(r_up_carts, r_dn_carts)``. Forwarded to the split-spin Jastrow + ratio kernel so it can skip per-call ``aos_*_old``/``W``/``U``/cross_vec + recomputation. Use the value carried in the projection's + ``Kinetic_streaming_state.j3_state``; pass ``None`` (default) for + the original 1-shot path used by observation/MCMC code. Returns: tuple[list[jax.Array], list[jax.Array], jax.Array, float]: @@ -1657,6 +1668,7 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): old_r_dn_carts=r_dn_carts, new_r_up_shifted=up_mesh_r_up, new_r_dn_shifted=dn_mesh_r_dn, + j3_state=j3_state, ) jastrow_ratio = jnp.asarray(jastrow_ratio, dtype=dtype_jnp) wf_ratio_all = det_ratio * jastrow_ratio diff --git a/jqmc/determinant.py b/jqmc/determinant.py index c5468473..a58d4ad9 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -2199,6 +2199,354 @@ def compute_grads_and_laplacian_ln_Det_fast( return grad_ln_D_up, grad_ln_D_dn, lap_ln_D_up, lap_ln_D_dn +# --------------------------------------------------------------------------- +# Streaming variant of ``compute_grads_and_laplacian_ln_Det_fast``. +# +# Maintains (a) AO/grad/lap tables, (b) λ_p ⨯ ao_dn intermediates, and +# (c) the full geminal grad/lap matrices, all consistent with the current +# (r_up, r_dn). A single-electron move advances them in O(n_ao² + n_ao·N_e +# + N_e²) per call, vs. O(n_ao²·N_e + n_ao·N_e²) for fresh recompute. +# +# See ``lrdmc_refactoring.md`` § 1-3 for the field list / advance derivation. +# --------------------------------------------------------------------------- + + +@struct.dataclass +class Det_streaming_state: + """Auxiliary tables required to evaluate ``∇ln|Det|`` / ``∇²ln|Det|`` + incrementally under single-electron moves. + + See ``lrdmc_refactoring.md`` § 1-3 for the per-field rationale. + """ + + ao_up: jax.Array + ao_dn: jax.Array + ao_up_grads: jax.Array + ao_dn_grads: jax.Array + ao_up_lap: jax.Array + ao_dn_lap: jax.Array + paired_dn: jax.Array + paired_dn_grads: jax.Array + paired_dn_lap: jax.Array + geminal_grad_up: jax.Array + geminal_grad_dn: jax.Array + geminal_lap_up: jax.Array + geminal_lap_dn: jax.Array + grad_ln_D_up: jax.Array + grad_ln_D_dn: jax.Array + lap_ln_D_up: jax.Array + lap_ln_D_dn: jax.Array + + +def _det_streaming_finalize( + geminal_grad_up: jax.Array, + geminal_grad_dn: jax.Array, + geminal_lap_up: jax.Array, + geminal_lap_dn: jax.Array, + G_inv: jax.Array, + n_dn: int, +) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]: + """Phase 3 of the fast path: contract ``geminal_*`` with ``G_inv``. + + Mirrors the tail of ``compute_grads_and_laplacian_ln_Det_fast`` so the + streaming and fresh paths produce bit-comparable results when fed + identical ``geminal_*`` and ``G_inv``. + """ + grad_ln_D_up_stack = jnp.einsum("gij,ji->gi", geminal_grad_up, G_inv) + grad_ln_D_dn_stack = jnp.einsum("ij,gji->gi", G_inv, geminal_grad_dn) + + grad_ln_D_up = grad_ln_D_up_stack.T + grad_ln_D_dn_full = grad_ln_D_dn_stack.T + + grad_ln_D_up_x, grad_ln_D_up_y, grad_ln_D_up_z = grad_ln_D_up_stack + grad_ln_D_dn_x, grad_ln_D_dn_y, grad_ln_D_dn_z = grad_ln_D_dn_stack + + lap_ln_D_up = -( + grad_ln_D_up_x * grad_ln_D_up_x + grad_ln_D_up_y * grad_ln_D_up_y + grad_ln_D_up_z * grad_ln_D_up_z + ) + jnp.einsum("ij,ji->i", geminal_lap_up, G_inv) + lap_ln_D_dn_full = -( + grad_ln_D_dn_x * grad_ln_D_dn_x + grad_ln_D_dn_y * grad_ln_D_dn_y + grad_ln_D_dn_z * grad_ln_D_dn_z + ) + jnp.einsum("ij,ji->i", G_inv, geminal_lap_dn) + + grad_ln_D_dn = grad_ln_D_dn_full[:n_dn] + lap_ln_D_dn = lap_ln_D_dn_full[:n_dn] + return grad_ln_D_up, grad_ln_D_dn, lap_ln_D_up, lap_ln_D_dn + + +@jit +def _init_grads_laplacian_ln_Det_streaming_state( + geminal_data: Geminal_data, + r_up_carts: jax.Array, + r_dn_carts: jax.Array, + geminal_inverse: jax.Array, +) -> Det_streaming_state: + """Initialize the det streaming state at ``(r_up, r_dn)``. + + Cost is dominated by the same ``λ_p ⨯ ao_dn`` and AO einsums as + :func:`compute_grads_and_laplacian_ln_Det_fast`; the streaming path + additionally retains the Phase-1/2 intermediates needed by the rank-1 + advance. + """ + if geminal_inverse is None: + raise ValueError("geminal_inverse must be provided for streaming init") + + dtype_jnp = get_dtype_jnp("det_grad_lap") + + lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) + + # Phase 0: AO/grad/lap evaluation (forward r_*_carts unchanged so the + # underlying kernels reconstruct r-R in fp64 — Principle 3b). + ao_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) + ao_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) + + ao_up_gx, ao_up_gy, ao_up_gz = geminal_data.compute_orb_grad_api(geminal_data.orb_data_up_spin, r_up_carts) + ao_dn_gx, ao_dn_gy, ao_dn_gz = geminal_data.compute_orb_grad_api(geminal_data.orb_data_dn_spin, r_dn_carts) + ao_up_grads = jnp.asarray(jnp.stack([ao_up_gx, ao_up_gy, ao_up_gz], axis=0), dtype=dtype_jnp) + ao_dn_grads = jnp.asarray(jnp.stack([ao_dn_gx, ao_dn_gy, ao_dn_gz], axis=0), dtype=dtype_jnp) + + ao_up_lap = jnp.asarray(geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_up_spin, r_up_carts), dtype=dtype_jnp) + ao_dn_lap = jnp.asarray(geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_dn_spin, r_dn_carts), dtype=dtype_jnp) + + # Phase 1: λ_p ⨯ ao_dn family (depends on dn only). + paired_dn = lambda_matrix_paired @ ao_dn + paired_dn_grads = jnp.einsum("ab,gbn->gan", lambda_matrix_paired, ao_dn_grads) + paired_dn_lap = lambda_matrix_paired @ ao_dn_lap + + # Phase 2: full geminal grad/lap matrices (paired ‖ unpaired hstack'd). + geminal_grad_up_paired = jnp.einsum("gia,aj->gij", jnp.swapaxes(ao_up_grads, 1, 2), paired_dn) + geminal_grad_up_unpaired = jnp.einsum("gia,ak->gik", jnp.swapaxes(ao_up_grads, 1, 2), lambda_matrix_unpaired) + geminal_grad_up = jnp.concatenate([geminal_grad_up_paired, geminal_grad_up_unpaired], axis=2) + + geminal_grad_dn_paired = jnp.einsum("ia,gaj->gij", ao_up.T, paired_dn_grads) + geminal_grad_dn_unpaired = jnp.zeros( + (3, geminal_data.num_electron_up, geminal_data.num_electron_up - geminal_data.num_electron_dn), + dtype=dtype_jnp, + ) + geminal_grad_dn = jnp.concatenate([geminal_grad_dn_paired, geminal_grad_dn_unpaired], axis=2) + + geminal_lap_up_paired = ao_up_lap.T @ paired_dn + geminal_lap_up_unpaired = ao_up_lap.T @ lambda_matrix_unpaired + geminal_lap_up = jnp.hstack([geminal_lap_up_paired, geminal_lap_up_unpaired]) + + geminal_lap_dn_paired = ao_up.T @ paired_dn_lap + geminal_lap_dn_unpaired = jnp.zeros( + (geminal_data.num_electron_up, geminal_data.num_electron_up - geminal_data.num_electron_dn), + dtype=dtype_jnp, + ) + geminal_lap_dn = jnp.hstack([geminal_lap_dn_paired, geminal_lap_dn_unpaired]) + + # Phase 3: contract with G_inv to expose per-electron grad/lap. + G_inv = geminal_inverse.astype(dtype_jnp) + grad_ln_D_up, grad_ln_D_dn, lap_ln_D_up, lap_ln_D_dn = _det_streaming_finalize( + geminal_grad_up, + geminal_grad_dn, + geminal_lap_up, + geminal_lap_dn, + G_inv, + geminal_data.num_electron_dn, + ) + + return Det_streaming_state( + ao_up=ao_up, + ao_dn=ao_dn, + ao_up_grads=ao_up_grads, + ao_dn_grads=ao_dn_grads, + ao_up_lap=ao_up_lap, + ao_dn_lap=ao_dn_lap, + paired_dn=paired_dn, + paired_dn_grads=paired_dn_grads, + paired_dn_lap=paired_dn_lap, + geminal_grad_up=geminal_grad_up, + geminal_grad_dn=geminal_grad_dn, + geminal_lap_up=geminal_lap_up, + geminal_lap_dn=geminal_lap_dn, + grad_ln_D_up=grad_ln_D_up, + grad_ln_D_dn=grad_ln_D_dn, + lap_ln_D_up=lap_ln_D_up, + lap_ln_D_dn=lap_ln_D_dn, + ) + + +@jit +def _advance_grads_laplacian_ln_Det_streaming_state( + geminal_data: Geminal_data, + state: Det_streaming_state, + moved_spin_is_up: jax.Array, + moved_index: jax.Array, + r_up_carts_new: jax.Array, + r_dn_carts_new: jax.Array, + A_new_inv: jax.Array, +) -> Det_streaming_state: + """Advance the det streaming state after a single-electron move. + + The new ``(r_up_carts_new, r_dn_carts_new)`` differ from the configuration + represented by ``state`` in *exactly one* electron position, identified by + ``(moved_spin_is_up, moved_index)``. ``A_new_inv`` is the Sherman-Morrison + inverse of ``G(r_up_new, r_dn_new)`` provided by the caller (typically + ``_body_step_core`` in ``jqmc_gfmc.py``). + + Cost: ``O(n_ao² + n_ao · N_e + N_e²)`` per call. + """ + dtype_jnp = get_dtype_jnp("det_grad_lap") + + lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) + + num_up = state.ao_up.shape[1] + num_dn = state.ao_dn.shape[1] + G_inv = A_new_inv.astype(dtype_jnp) + + def _branch_up(_): + # --- Phase 0: single-point AO eval at r_up_new[k] ----------------- + r_new = jnp.expand_dims(r_up_carts_new[moved_index], axis=0) # (1, 3) + ao_col = jnp.asarray( + geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_new)[:, 0], + dtype=dtype_jnp, + ) # (n_ao_up,) + gx, gy, gz = geminal_data.compute_orb_grad_api(geminal_data.orb_data_up_spin, r_new) + grad_col = jnp.asarray(jnp.stack([gx[:, 0], gy[:, 0], gz[:, 0]], axis=0), dtype=dtype_jnp) # (3, n_ao_up) + lap_col = jnp.asarray( + geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_up_spin, r_new)[:, 0], + dtype=dtype_jnp, + ) # (n_ao_up,) + + new_ao_up = state.ao_up.at[:, moved_index].set(ao_col) + new_ao_up_grads = state.ao_up_grads.at[:, :, moved_index].set(grad_col) + new_ao_up_lap = state.ao_up_lap.at[:, moved_index].set(lap_col) + + # --- Phase 2: row k of geminal_* (paired ‖ unpaired) -------------- + # row of geminal_grad_up: einsum("ga,aj->gj", grad_col, paired_dn) ‖ + # einsum("ga,ak->gk", grad_col, λ_u) + row_grad_up_paired = jnp.einsum("ga,aj->gj", grad_col, state.paired_dn) + row_grad_up_unpaired = jnp.einsum("ga,ak->gk", grad_col, lambda_matrix_unpaired) + row_grad_up = jnp.concatenate([row_grad_up_paired, row_grad_up_unpaired], axis=1) + new_geminal_grad_up = state.geminal_grad_up.at[:, moved_index, :].set(row_grad_up) + + # row of geminal_grad_dn: paired = einsum("a,gaj->gj", ao_col, paired_dn_grads), + # unpaired = zeros. + row_grad_dn_paired = jnp.einsum("a,gaj->gj", ao_col, state.paired_dn_grads) + row_grad_dn_unpaired = jnp.zeros((3, num_up - num_dn), dtype=dtype_jnp) + row_grad_dn = jnp.concatenate([row_grad_dn_paired, row_grad_dn_unpaired], axis=1) + new_geminal_grad_dn = state.geminal_grad_dn.at[:, moved_index, :].set(row_grad_dn) + + # row of geminal_lap_up: lap_col @ paired_dn ‖ lap_col @ λ_u + row_lap_up_paired = lap_col @ state.paired_dn + row_lap_up_unpaired = lap_col @ lambda_matrix_unpaired + row_lap_up = jnp.concatenate([row_lap_up_paired, row_lap_up_unpaired], axis=0) + new_geminal_lap_up = state.geminal_lap_up.at[moved_index, :].set(row_lap_up) + + # row of geminal_lap_dn: ao_col @ paired_dn_lap ‖ zeros + row_lap_dn_paired = ao_col @ state.paired_dn_lap + row_lap_dn_unpaired = jnp.zeros((num_up - num_dn,), dtype=dtype_jnp) + row_lap_dn = jnp.concatenate([row_lap_dn_paired, row_lap_dn_unpaired], axis=0) + new_geminal_lap_dn = state.geminal_lap_dn.at[moved_index, :].set(row_lap_dn) + + grad_ln_D_up, grad_ln_D_dn, lap_ln_D_up, lap_ln_D_dn = _det_streaming_finalize( + new_geminal_grad_up, + new_geminal_grad_dn, + new_geminal_lap_up, + new_geminal_lap_dn, + G_inv, + num_dn, + ) + + return state.replace( + ao_up=new_ao_up, + ao_up_grads=new_ao_up_grads, + ao_up_lap=new_ao_up_lap, + geminal_grad_up=new_geminal_grad_up, + geminal_grad_dn=new_geminal_grad_dn, + geminal_lap_up=new_geminal_lap_up, + geminal_lap_dn=new_geminal_lap_dn, + grad_ln_D_up=grad_ln_D_up, + grad_ln_D_dn=grad_ln_D_dn, + lap_ln_D_up=lap_ln_D_up, + lap_ln_D_dn=lap_ln_D_dn, + ) + + def _branch_dn(_): + # --- Phase 0: single-point AO eval at r_dn_new[k] ----------------- + r_new = jnp.expand_dims(r_dn_carts_new[moved_index], axis=0) + ao_col = jnp.asarray( + geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_new)[:, 0], + dtype=dtype_jnp, + ) # (n_ao_dn,) + gx, gy, gz = geminal_data.compute_orb_grad_api(geminal_data.orb_data_dn_spin, r_new) + grad_col = jnp.asarray(jnp.stack([gx[:, 0], gy[:, 0], gz[:, 0]], axis=0), dtype=dtype_jnp) # (3, n_ao_dn) + lap_col = jnp.asarray( + geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_dn_spin, r_new)[:, 0], + dtype=dtype_jnp, + ) # (n_ao_dn,) + + new_ao_dn = state.ao_dn.at[:, moved_index].set(ao_col) + new_ao_dn_grads = state.ao_dn_grads.at[:, :, moved_index].set(grad_col) + new_ao_dn_lap = state.ao_dn_lap.at[:, moved_index].set(lap_col) + + # --- Phase 1: column k of paired_dn family ------------------------ + new_paired_dn_col = lambda_matrix_paired @ ao_col # (n_ao_up,) + new_paired_dn = state.paired_dn.at[:, moved_index].set(new_paired_dn_col) + + new_paired_dn_grads_col = jnp.einsum("ab,gb->ga", lambda_matrix_paired, grad_col) # (3, n_ao_up) + new_paired_dn_grads = state.paired_dn_grads.at[:, :, moved_index].set(new_paired_dn_grads_col) + + new_paired_dn_lap_col = lambda_matrix_paired @ lap_col # (n_ao_up,) + new_paired_dn_lap = state.paired_dn_lap.at[:, moved_index].set(new_paired_dn_lap_col) + + # --- Phase 2: column k of geminal_* (paired block only; unpaired + # columns are independent of dn). -------------------- + # geminal_grad_up[:, :, k] paired-block update: + col_grad_up = jnp.einsum("gia,a->gi", jnp.swapaxes(state.ao_up_grads, 1, 2), new_paired_dn_col) + new_geminal_grad_up = state.geminal_grad_up.at[:, :, moved_index].set(col_grad_up) + + # geminal_grad_dn[:, :, k] paired-block update: + col_grad_dn = jnp.einsum("ai,ga->gi", state.ao_up, new_paired_dn_grads_col) + new_geminal_grad_dn = state.geminal_grad_dn.at[:, :, moved_index].set(col_grad_dn) + + # geminal_lap_up[:, k] paired-block update: + col_lap_up = state.ao_up_lap.T @ new_paired_dn_col + new_geminal_lap_up = state.geminal_lap_up.at[:, moved_index].set(col_lap_up) + + # geminal_lap_dn[:, k] paired-block update: + col_lap_dn = state.ao_up.T @ new_paired_dn_lap_col + new_geminal_lap_dn = state.geminal_lap_dn.at[:, moved_index].set(col_lap_dn) + + grad_ln_D_up, grad_ln_D_dn, lap_ln_D_up, lap_ln_D_dn = _det_streaming_finalize( + new_geminal_grad_up, + new_geminal_grad_dn, + new_geminal_lap_up, + new_geminal_lap_dn, + G_inv, + num_dn, + ) + + return state.replace( + ao_dn=new_ao_dn, + ao_dn_grads=new_ao_dn_grads, + ao_dn_lap=new_ao_dn_lap, + paired_dn=new_paired_dn, + paired_dn_grads=new_paired_dn_grads, + paired_dn_lap=new_paired_dn_lap, + geminal_grad_up=new_geminal_grad_up, + geminal_grad_dn=new_geminal_grad_dn, + geminal_lap_up=new_geminal_lap_up, + geminal_lap_dn=new_geminal_lap_dn, + grad_ln_D_up=grad_ln_D_up, + grad_ln_D_dn=grad_ln_D_dn, + lap_ln_D_up=lap_ln_D_up, + lap_ln_D_dn=lap_ln_D_dn, + ) + + # Edge case: zero-electron spin sectors collapse the cond. + if num_up == 0: + return _branch_dn(None) + if num_dn == 0: + return _branch_up(None) + return jax.lax.cond(moved_spin_is_up, _branch_up, _branch_dn, operand=None) + + def _compute_grads_and_laplacian_ln_Det_fast_debug( geminal_data: Geminal_data, r_up_carts: jax.Array, diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 605c1bc0..dd64226a 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -1027,6 +1027,79 @@ def _grad_lap_one_spin(r_carts): return grad_up, grad_dn, lap_up, lap_dn +# --------------------------------------------------------------------------- +# J1 streaming state (PR3). +# +# J1 is a per-electron sum over atoms with no inter-electron coupling, so a +# single-electron move only affects one row of the per-electron grad/lap. +# The state simply caches those rows; advance recomputes the moved row in +# ``O(n_atom)`` rather than the fresh O(N_e * n_atom). +# --------------------------------------------------------------------------- + + +@struct.dataclass +class Jastrow_one_body_streaming_state: + """Cached per-electron J1 grad/lap consistent with current ``(r_up, r_dn)``.""" + + grad_J1_up: jax.Array # (N_up, 3) + grad_J1_dn: jax.Array # (N_dn, 3) + lap_J1_up: jax.Array # (N_up,) + lap_J1_dn: jax.Array # (N_dn,) + + +@jit +def _init_grads_laplacian_Jastrow_one_body_streaming_state( + jastrow_one_body_data: Jastrow_one_body_data, + r_up_carts: jax.Array, + r_dn_carts: jax.Array, +) -> Jastrow_one_body_streaming_state: + """One-shot init equivalent in cost to ``compute_grads_and_laplacian_Jastrow_one_body``.""" + g_up, g_dn, l_up, l_dn = compute_grads_and_laplacian_Jastrow_one_body(jastrow_one_body_data, r_up_carts, r_dn_carts) + return Jastrow_one_body_streaming_state(grad_J1_up=g_up, grad_J1_dn=g_dn, lap_J1_up=l_up, lap_J1_dn=l_dn) + + +@jit +def _advance_grads_laplacian_Jastrow_one_body_streaming_state( + jastrow_one_body_data: Jastrow_one_body_data, + state: Jastrow_one_body_streaming_state, + moved_spin_is_up: jax.Array, + moved_index: jax.Array, + r_up_carts_new: jax.Array, + r_dn_carts_new: jax.Array, +) -> Jastrow_one_body_streaming_state: + """Advance J1 state after a single-electron move. + + Recomputes one electron's row by re-running the existing one-spin kernel + on a single-row slice. Cost: ``O(n_atom)``. + """ + num_up = state.grad_J1_up.shape[0] + num_dn = state.grad_J1_dn.shape[0] + + def _branch_up(_): + # Reuse the full-batch kernel on a length-1 slice — gives one row that + # we slot back into the cached state. + r_slice = jnp.expand_dims(r_up_carts_new[moved_index], axis=0) # (1, 3) + g_row, _, l_row, _ = compute_grads_and_laplacian_Jastrow_one_body(jastrow_one_body_data, r_slice, r_slice[:0]) + new_grad = state.grad_J1_up.at[moved_index].set(g_row[0]) + new_lap = state.lap_J1_up.at[moved_index].set(l_row[0]) + return state.replace(grad_J1_up=new_grad, lap_J1_up=new_lap) + + def _branch_dn(_): + r_slice = jnp.expand_dims(r_dn_carts_new[moved_index], axis=0) + # Pass empty up so only the dn branch contributes (J1 is per-spin + # independent — feeding empty up has zero effect on dn output). + _, g_row, _, l_row = compute_grads_and_laplacian_Jastrow_one_body(jastrow_one_body_data, r_slice[:0], r_slice) + new_grad = state.grad_J1_dn.at[moved_index].set(g_row[0]) + new_lap = state.lap_J1_dn.at[moved_index].set(l_row[0]) + return state.replace(grad_J1_dn=new_grad, lap_J1_dn=new_lap) + + if num_up == 0: + return _branch_dn(None) + if num_dn == 0: + return _branch_up(None) + return jax.lax.cond(moved_spin_is_up, _branch_up, _branch_dn, operand=None) + + @struct.dataclass class Jastrow_two_body_data: r"""Two-body Jastrow parameter container. @@ -2153,6 +2226,7 @@ def _compute_ratio_Jastrow_part_rank1_update( old_r_dn_carts: jax.Array, new_r_up_carts_arr: jax.Array, new_r_dn_carts_arr: jax.Array, + j3_state: "Jastrow_three_body_streaming_state | None" = None, ) -> jax.Array: r"""Compute :math:`\exp(J(\mathbf r'))/\exp(J(\mathbf r))` for batched moves. @@ -2166,6 +2240,12 @@ def _compute_ratio_Jastrow_part_rank1_update( old_r_dn_carts: Reference spin-down coordinates with shape ``(N_dn, 3)``. new_r_up_carts_arr: Proposed spin-up coordinates with shape ``(N_grid, N_up, 3)``. new_r_dn_carts_arr: Proposed spin-down coordinates with shape ``(N_grid, N_dn, 3)``. + j3_state: Optional cached J3 auxiliaries consistent with + ``(old_r_up_carts, old_r_dn_carts)``. When provided, the J3 block + reuses ``aos_*``, ``j3_mat @ aos_*``, ``j3_mat.T @ aos_*`` from the + state instead of recomputing them — saves per-call ``O(n_ao^2 * N_e)`` + in matmul work. Pass ``None`` (default) to recompute from scratch + (the original 1-shot path used outside the projection loop). Returns: jax.Array: Jastrow ratios per grid with shape ``(N_grid,)`` (includes ``exp``). @@ -2485,9 +2565,17 @@ def _batch_pairwise_sum(points_a, points_b, param): j3_mat = j3d._j_matrix_jnp[:, :-1] # (n_ao, n_ao) shared for up-up / dn-dn / up-dn j1_vec = j3d._j_matrix_jnp[:, -1] # (n_ao,) - # Old AOs evaluated once - aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype_jnp) # (n_ao, N_up) - aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts), dtype=dtype_jnp) # (n_ao, N_dn) + # Old AOs evaluated once. + # When ``j3_state`` is supplied, the cached AOs/W/U/cross_vec from the + # streaming state are consistent with ``(old_r_up_carts, old_r_dn_carts)`` + # by contract — we just dtype-cast into the jastrow_ratio zone and skip + # the recomputation. Python-static dispatch (j3_state is None vs not). + if j3_state is None: + aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype_jnp) # (n_ao, N_up) + aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts), dtype=dtype_jnp) # (n_ao, N_dn) + else: + aos_up_old = j3_state.aos_up.astype(dtype_jnp) + aos_dn_old = j3_state.aos_dn.astype(dtype_jnp) N_batch = new_r_up_carts_arr.shape[0] @@ -2511,13 +2599,29 @@ def _batch_pairwise_sum(points_a, points_b, param): aos_old_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_old_moved), dtype=dtype_jnp) # (n_ao, N) aos_p_batch = aos_new_batch - aos_old_batch # (n_ao, N) - # Precompute constant products (independent of config) - W_up = jnp.dot(j3_mat, aos_up_old) # (n_ao, N_up) = j3_mat @ A_up - U_up = jnp.dot(aos_up_old.T, j3_mat) # (N_up, n_ao) = A_up.T @ j3_mat - W_dn = jnp.dot(j3_mat, aos_dn_old) # (n_ao, N_dn) - U_dn = jnp.dot(aos_dn_old.T, j3_mat) # (N_dn, n_ao) - dn_cross_vec = j3_mat @ jnp.sum(aos_dn_old, axis=1) # (n_ao,): UP cross term constant - up_cross_vec = jnp.sum(aos_up_old, axis=1) @ j3_mat # (n_ao,): DN cross term constant + # Precompute constant products (independent of config). With a + # streaming state, all four matmuls are read directly from the cache + # — that's the main per-step ``O(n_ao^2 * N_e)`` saving. Note that + # ``j3_state.j3_mat_T_aos_*`` stores ``j3_mat.T @ aos_*`` of shape + # ``(n_ao, N_*)``, while we want ``U_* = aos_*.T @ j3_mat`` of shape + # ``(N_*, n_ao)`` — these are transposes of each other. + if j3_state is None: + W_up = jnp.dot(j3_mat, aos_up_old) # (n_ao, N_up) = j3_mat @ A_up + U_up = jnp.dot(aos_up_old.T, j3_mat) # (N_up, n_ao) = A_up.T @ j3_mat + W_dn = jnp.dot(j3_mat, aos_dn_old) # (n_ao, N_dn) + U_dn = jnp.dot(aos_dn_old.T, j3_mat) # (N_dn, n_ao) + dn_cross_vec = j3_mat @ jnp.sum(aos_dn_old, axis=1) # (n_ao,): UP cross term constant + up_cross_vec = jnp.sum(aos_up_old, axis=1) @ j3_mat # (n_ao,): DN cross term constant + else: + W_up = j3_state.j3_mat_aos_up.astype(dtype_jnp) + W_dn = j3_state.j3_mat_aos_dn.astype(dtype_jnp) + U_up = j3_state.j3_mat_T_aos_up.astype(dtype_jnp).T + U_dn = j3_state.j3_mat_T_aos_dn.astype(dtype_jnp).T + # cross_vec equivalences: + # j3_mat @ sum(aos_dn, axis=1) = sum(j3_mat @ aos_dn, axis=1) = sum(W_dn, axis=1) + # sum(aos_up, axis=1) @ j3_mat = sum(j3_mat.T @ aos_up, axis=1) = sum(j3_mat_T_aos_up, axis=1) + dn_cross_vec = jnp.sum(W_dn, axis=1) + up_cross_vec = jnp.sum(j3_state.j3_mat_T_aos_up.astype(dtype_jnp), axis=1) # Q index: idx_up for UP configs, idx_dn for DN configs idx_for_Q = jnp.where(up_moved_batch, idx_up, idx_dn) # (N,) @@ -2581,6 +2685,7 @@ def _compute_ratio_Jastrow_part_split_spin( old_r_dn_carts: jax.Array, new_r_up_shifted: jax.Array, new_r_dn_shifted: jax.Array, + j3_state: "Jastrow_three_body_streaming_state | None" = None, ) -> jax.Array: r"""Jastrow ratio for a block-structured mesh where up and dn electrons move separately. @@ -2593,6 +2698,11 @@ def _compute_ratio_Jastrow_part_split_spin( already-computed ``aos_up_old`` / ``aos_dn_old`` matrices, avoiding two extra ``compute_orb_api`` calls. + When called from the projection streaming path, the caller may pass + ``j3_state`` to skip recomputing ``aos_*_old`` and the ``W``/``U``/cross_vec + products — see ``_compute_ratio_Jastrow_part_rank1_update`` for the exact + correspondence. + Args: jastrow_data: Active Jastrow components. old_r_up_carts: Reference up-spin coordinates ``(N_up, 3)``. @@ -2637,7 +2747,14 @@ def _compute_ratio_Jastrow_part_split_spin( [jnp.broadcast_to(old_r_dn_carts[None], (g_up, num_dn, 3)), new_r_dn_shifted], axis=0, ) - return _compute_ratio_Jastrow_part_rank1_update(jastrow_data, old_r_up_carts, old_r_dn_carts, combined_up, combined_dn) + return _compute_ratio_Jastrow_part_rank1_update( + jastrow_data, + old_r_up_carts, + old_r_dn_carts, + combined_up, + combined_dn, + j3_state=j3_state, + ) g_up = new_r_up_shifted.shape[0] g_dn = new_r_dn_shifted.shape[0] @@ -2746,16 +2863,27 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: j1_vec = j3d._j_matrix_jnp[:, -1] # (n_ao,) # Old AOs evaluated once; column slices give old AO at each moved position. - aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype_jnp) # (n_ao, N_up) - aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts), dtype=dtype_jnp) # (n_ao, N_dn) - - # Precompute constant products (shared between blocks). - W_up = jnp.dot(j3_mat, aos_up_old) # (n_ao, N_up) - U_up = jnp.dot(aos_up_old.T, j3_mat) # (N_up, n_ao) - W_dn = jnp.dot(j3_mat, aos_dn_old) # (n_ao, N_dn) - U_dn = jnp.dot(aos_dn_old.T, j3_mat) # (N_dn, n_ao) - dn_cross_vec = j3_mat @ jnp.sum(aos_dn_old, axis=1) # (n_ao,): UP cross term constant - up_cross_vec = jnp.sum(aos_up_old, axis=1) @ j3_mat # (n_ao,): DN cross term constant + # When the streaming state is provided, all four matmuls and both + # cross_vec products come from the cache (Python-static dispatch). + if j3_state is None: + aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype_jnp) # (n_ao, N_up) + aos_dn_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_dn_carts), dtype=dtype_jnp) # (n_ao, N_dn) + # Precompute constant products (shared between blocks). + W_up = jnp.dot(j3_mat, aos_up_old) # (n_ao, N_up) + U_up = jnp.dot(aos_up_old.T, j3_mat) # (N_up, n_ao) + W_dn = jnp.dot(j3_mat, aos_dn_old) # (n_ao, N_dn) + U_dn = jnp.dot(aos_dn_old.T, j3_mat) # (N_dn, n_ao) + dn_cross_vec = j3_mat @ jnp.sum(aos_dn_old, axis=1) # (n_ao,): UP cross term constant + up_cross_vec = jnp.sum(aos_up_old, axis=1) @ j3_mat # (n_ao,): DN cross term constant + else: + aos_up_old = j3_state.aos_up.astype(dtype_jnp) + aos_dn_old = j3_state.aos_dn.astype(dtype_jnp) + W_up = j3_state.j3_mat_aos_up.astype(dtype_jnp) + W_dn = j3_state.j3_mat_aos_dn.astype(dtype_jnp) + U_up = j3_state.j3_mat_T_aos_up.astype(dtype_jnp).T + U_dn = j3_state.j3_mat_T_aos_dn.astype(dtype_jnp).T + dn_cross_vec = jnp.sum(W_dn, axis=1) + up_cross_vec = jnp.sum(j3_state.j3_mat_T_aos_up.astype(dtype_jnp), axis=1) # ── UP BLOCK ───────────────────────────────────────────────────────── # New AOs at the moved up-electron positions; old AOs by column-slice. @@ -3354,6 +3482,194 @@ def pair_terms(diff): return grad_up, grad_dn, lap_up, lap_dn +# --------------------------------------------------------------------------- +# J2 streaming state (PR3). +# +# When a single electron k of spin σ moves, only pair contributions involving +# k change. The state caches per-electron grad/lap and the previous (r_up, +# r_dn) so the advance can compute the per-pair delta for that electron. +# Cost: O(N_e) per advance, vs O(N_e²) fresh. +# --------------------------------------------------------------------------- + + +@struct.dataclass +class Jastrow_two_body_streaming_state: + """Cached J2 grad/lap and electron coordinates consistent with the state.""" + + r_up_carts: jax.Array # (N_up, 3) — config used for the cached J2 quantities + r_dn_carts: jax.Array # (N_dn, 3) + grad_J2_up: jax.Array # (N_up, 3) + grad_J2_dn: jax.Array # (N_dn, 3) + lap_J2_up: jax.Array # (N_up,) + lap_J2_dn: jax.Array # (N_dn,) + + +def _j2_pair_terms(j2b_type: str, a: jax.Array, eps: jax.Array, diff: jax.Array): + """Single-pair grad / lap contributions for the two-body Jastrow. + + Mirrors the closures inside ``compute_grads_and_laplacian_Jastrow_two_body`` + so init and advance share the exact arithmetic. ``j2b_type`` is JIT-static + (Jastrow_two_body_data marks it ``pytree_node=False``). + """ + r = jnp.sqrt(jnp.sum(diff * diff, axis=-1)) + r = jnp.maximum(r, eps) + if j2b_type == "pade": + denom = 1.0 + a * r + f_prime = 0.5 / (denom * denom) + grad_coeff = f_prime / r + lap = -a / (denom * denom * denom) + (2.0 * f_prime) / r + elif j2b_type == "exp": + exp_term = jnp.exp(-a * r) + f_prime = 0.5 * exp_term + grad_coeff = f_prime / r + lap = -(a / 2.0) * exp_term + (2.0 * f_prime) / r + else: + raise ValueError(f"Unknown jastrow_2b_type: {j2b_type}") + return grad_coeff[..., None] * diff, lap + + +@jit +def _init_grads_laplacian_Jastrow_two_body_streaming_state( + jastrow_two_body_data: Jastrow_two_body_data, + r_up_carts: jax.Array, + r_dn_carts: jax.Array, +) -> Jastrow_two_body_streaming_state: + """Build a J2 state at ``(r_up, r_dn)`` via the existing fresh kernel.""" + g_up, g_dn, l_up, l_dn = compute_grads_and_laplacian_Jastrow_two_body(jastrow_two_body_data, r_up_carts, r_dn_carts) + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + return Jastrow_two_body_streaming_state( + r_up_carts=jnp.asarray(r_up_carts, dtype=dtype_jnp), + r_dn_carts=jnp.asarray(r_dn_carts, dtype=dtype_jnp), + grad_J2_up=g_up, + grad_J2_dn=g_dn, + lap_J2_up=l_up, + lap_J2_dn=l_dn, + ) + + +@jit +def _advance_grads_laplacian_Jastrow_two_body_streaming_state( + jastrow_two_body_data: Jastrow_two_body_data, + state: Jastrow_two_body_streaming_state, + moved_spin_is_up: jax.Array, + moved_index: jax.Array, + r_up_carts_new: jax.Array, + r_dn_carts_new: jax.Array, +) -> Jastrow_two_body_streaming_state: + """Advance J2 state after a single-electron move. + + Computes only the pair-contribution deltas that involve the moved + electron: ``O(N_e)`` operations per call. + """ + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + j2b_type = jastrow_two_body_data.jastrow_2b_type + a = jnp.asarray(jastrow_two_body_data.jastrow_2b_param, dtype=dtype_jnp) + eps = jnp.asarray(EPS_safe_distance, dtype=dtype_jnp) + + num_up = state.r_up_carts.shape[0] + num_dn = state.r_dn_carts.shape[0] + + def _branch_up(_): + r_old = state.r_up_carts[moved_index] + r_new = r_up_carts_new[moved_index] + + # --- Same-spin (up-up) pairs (k, i) for i ≠ k -------------------- + # Old & new diffs both place 0 at i=k (state.r_up_carts[k]=r_old vs r_old, + # r_up_carts_new[k]=r_new vs r_new), so masking is implicit at i=k for new + # but not for old. Mask out the i=k row explicitly to avoid contaminating k + # via the eps-clamped self-pair value. + diff_old_uu = r_old - state.r_up_carts # (N_up, 3); k-th = 0 + diff_new_uu = r_new - r_up_carts_new # (N_up, 3); k-th = 0 + grad_old_uu, lap_old_uu = _j2_pair_terms(j2b_type, a, eps, diff_old_uu) + grad_new_uu, lap_new_uu = _j2_pair_terms(j2b_type, a, eps, diff_new_uu) + delta_grad_uu = grad_new_uu - grad_old_uu # (N_up, 3) + delta_lap_uu = lap_new_uu - lap_old_uu # (N_up,) + mask_uu = (jnp.arange(num_up) != moved_index).astype(dtype_jnp) + delta_grad_uu = delta_grad_uu * mask_uu[:, None] + delta_lap_uu = delta_lap_uu * mask_uu + + # i ≠ k: grad_up[i] -= delta_grad_uu[i], lap_up[i] += delta_lap_uu[i] + new_grad_up = state.grad_J2_up - delta_grad_uu + new_lap_up = state.lap_J2_up + delta_lap_uu + # k: grad_up[k] += sum delta_grad_uu, lap_up[k] += sum delta_lap_uu + new_grad_up = new_grad_up.at[moved_index].add(jnp.sum(delta_grad_uu, axis=0)) + new_lap_up = new_lap_up.at[moved_index].add(jnp.sum(delta_lap_uu, axis=0)) + + # --- Cross-spin (up-dn) pairs (k, j) for all j ------------------- + diff_old_ud = r_old[None, :] - state.r_dn_carts # (N_dn, 3) + diff_new_ud = r_new[None, :] - state.r_dn_carts # (N_dn, 3) (r_dn unchanged) + grad_old_ud, lap_old_ud = _j2_pair_terms(j2b_type, a, eps, diff_old_ud) + grad_new_ud, lap_new_ud = _j2_pair_terms(j2b_type, a, eps, diff_new_ud) + delta_grad_ud = grad_new_ud - grad_old_ud + delta_lap_ud = lap_new_ud - lap_old_ud + + new_grad_up = new_grad_up.at[moved_index].add(jnp.sum(delta_grad_ud, axis=0)) + new_lap_up = new_lap_up.at[moved_index].add(jnp.sum(delta_lap_ud, axis=0)) + new_grad_dn = state.grad_J2_dn - delta_grad_ud + new_lap_dn = state.lap_J2_dn + delta_lap_ud + + new_r_up = state.r_up_carts.at[moved_index].set(r_new) + return state.replace( + r_up_carts=new_r_up, + grad_J2_up=new_grad_up, + grad_J2_dn=new_grad_dn, + lap_J2_up=new_lap_up, + lap_J2_dn=new_lap_dn, + ) + + def _branch_dn(_): + r_old = state.r_dn_carts[moved_index] + r_new = r_dn_carts_new[moved_index] + + # --- Same-spin (dn-dn) ------------------------------------------- + diff_old_dd = r_old - state.r_dn_carts + diff_new_dd = r_new - r_dn_carts_new + grad_old_dd, lap_old_dd = _j2_pair_terms(j2b_type, a, eps, diff_old_dd) + grad_new_dd, lap_new_dd = _j2_pair_terms(j2b_type, a, eps, diff_new_dd) + delta_grad_dd = grad_new_dd - grad_old_dd + delta_lap_dd = lap_new_dd - lap_old_dd + mask_dd = (jnp.arange(num_dn) != moved_index).astype(dtype_jnp) + delta_grad_dd = delta_grad_dd * mask_dd[:, None] + delta_lap_dd = delta_lap_dd * mask_dd + + new_grad_dn = state.grad_J2_dn - delta_grad_dd + new_lap_dn = state.lap_J2_dn + delta_lap_dd + new_grad_dn = new_grad_dn.at[moved_index].add(jnp.sum(delta_grad_dd, axis=0)) + new_lap_dn = new_lap_dn.at[moved_index].add(jnp.sum(delta_lap_dd, axis=0)) + + # --- Cross-spin (up-dn): grad_up[i] receives +grad_pair(r_up[i] - r_dn[k]) + # so for dn-k moving, the deltas flip signs vs the up branch: + # diff = r_up[i] - r_dn_* → diff_new for r_dn[k]=r_new is r_up[i] - r_new + diff_old_du = state.r_up_carts - r_old[None, :] # (N_up, 3) + diff_new_du = state.r_up_carts - r_new[None, :] # (N_up, 3) + grad_old_du, lap_old_du = _j2_pair_terms(j2b_type, a, eps, diff_old_du) + grad_new_du, lap_new_du = _j2_pair_terms(j2b_type, a, eps, diff_new_du) + delta_grad_du = grad_new_du - grad_old_du + delta_lap_du = lap_new_du - lap_old_du + + # grad_up[i] += delta_grad_du[i] (sign +) + new_grad_up = state.grad_J2_up + delta_grad_du + new_lap_up = state.lap_J2_up + delta_lap_du + # grad_dn[k] -= sum_i delta_grad_du[i] (sign − accumulated at k) + new_grad_dn = new_grad_dn.at[moved_index].add(-jnp.sum(delta_grad_du, axis=0)) + new_lap_dn = new_lap_dn.at[moved_index].add(jnp.sum(delta_lap_du, axis=0)) + + new_r_dn = state.r_dn_carts.at[moved_index].set(r_new) + return state.replace( + r_dn_carts=new_r_dn, + grad_J2_up=new_grad_up, + grad_J2_dn=new_grad_dn, + lap_J2_up=new_lap_up, + lap_J2_dn=new_lap_dn, + ) + + if num_up == 0: + return _branch_dn(None) + if num_dn == 0: + return _branch_up(None) + return jax.lax.cond(moved_spin_is_up, _branch_up, _branch_dn, operand=None) + + def _compute_grads_and_laplacian_Jastrow_two_body_debug( jastrow_two_body_data: Jastrow_two_body_data, r_up_carts: np.ndarray, @@ -3746,6 +4062,306 @@ def compute_grads_and_laplacian_Jastrow_three_body( return grad_J3_up, grad_J3_dn, lap_up_contrib, lap_dn_contrib +# --------------------------------------------------------------------------- +# J3 streaming state (single-electron rank-1 advance for projection loops) +# --------------------------------------------------------------------------- +# +# The functions below maintain a per-walker auxiliary state that lets us +# advance the per-electron J3 gradients/Laplacians by O(n_ao^2 + n_ao*N_e) +# per single-electron move, instead of recomputing them from scratch at +# O(n_ao^2 * N_e + n_ao * N_e^2). Used by the GFMC projection inner loop +# (jqmc_gfmc.py:_body_fun_n_streaming). +# +# Design references: lrdmc_refactoring.md sections 1-1, 1-2, 1-4. +# +# Lifetime: the state is freshly initialized at each branching boundary +# (when _projection_n is re-entered) and advanced for at most +# `num_mcmc_per_measurement` steps inside the fori_loop, mirroring the +# Sherman-Morrison `A_old_inv`. No persistence across branchings. + + +@struct.dataclass +class Jastrow_three_body_streaming_state: + """Auxiliary state for streaming J3 grad/Laplacian updates. + + All fields are evaluated at the current ``(r_up_carts, r_dn_carts)``. + Advancing the state via :func:`_advance_grads_laplacian_Jastrow_three_body_streaming_state` + after a single-electron move keeps every field consistent with the new + configuration, with cost ``O(n_ao^2 + n_ao * N_e)`` per step. + + Fields (shapes use ``n_orb`` for the orbital dimension; for MO-based + three-body the same ``n_orb`` is used, since orbitals are evaluated by + ``compute_MOs`` to dimension ``orb_data._num_orb``): + + - ``aos_up`` / ``aos_dn``: ``(n_orb, N_up)`` / ``(n_orb, N_dn)`` orbital values. + - ``grad_aos_up`` / ``grad_aos_dn``: ``(n_orb, N_up, 3)`` / ``(n_orb, N_dn, 3)``. + - ``lap_aos_up`` / ``lap_aos_dn``: ``(n_orb, N_up)`` / ``(n_orb, N_dn)``. + - ``j3_mat_aos_up`` / ``j3_mat_aos_dn``: ``j3_mat @ aos_*`` (shapes match aos_*). + - ``j3_mat_T_aos_up`` / ``j3_mat_T_aos_dn``: ``j3_mat.T @ aos_*``. + - ``g_up`` / ``g_dn``: ``(n_orb, N_up)`` / ``(n_orb, N_dn)`` ``dJ/dA`` per electron. + - ``grad_J3_up`` / ``grad_J3_dn``: ``(N_up, 3)`` / ``(N_dn, 3)`` per-electron grad. + - ``lap_J3_up`` / ``lap_J3_dn``: ``(N_up,)`` / ``(N_dn,)`` per-electron lap. + """ + + aos_up: jax.Array = struct.field(pytree_node=True) + aos_dn: jax.Array = struct.field(pytree_node=True) + grad_aos_up: jax.Array = struct.field(pytree_node=True) + grad_aos_dn: jax.Array = struct.field(pytree_node=True) + lap_aos_up: jax.Array = struct.field(pytree_node=True) + lap_aos_dn: jax.Array = struct.field(pytree_node=True) + j3_mat_aos_up: jax.Array = struct.field(pytree_node=True) + j3_mat_aos_dn: jax.Array = struct.field(pytree_node=True) + j3_mat_T_aos_up: jax.Array = struct.field(pytree_node=True) + j3_mat_T_aos_dn: jax.Array = struct.field(pytree_node=True) + g_up: jax.Array = struct.field(pytree_node=True) + g_dn: jax.Array = struct.field(pytree_node=True) + grad_J3_up: jax.Array = struct.field(pytree_node=True) + grad_J3_dn: jax.Array = struct.field(pytree_node=True) + lap_J3_up: jax.Array = struct.field(pytree_node=True) + lap_J3_dn: jax.Array = struct.field(pytree_node=True) + + +def _three_body_orb_apis(jastrow_three_body_data: Jastrow_three_body_data): + """Pick the correct orbital evaluation backends (AO or MO). + + Returned as a Python tuple so callers can dispatch statically (the + orb_data type is JIT-static via the @struct.dataclass). + """ + orb_data = jastrow_three_body_data.orb_data + if isinstance(orb_data, MOs_data): + return compute_MOs, compute_MOs_grad, compute_MOs_laplacian + if isinstance(orb_data, (AOs_sphe_data, AOs_cart_data)): + return compute_AOs, compute_AOs_grad, compute_AOs_laplacian + raise NotImplementedError(f"Unsupported orb_data type: {type(orb_data)}") + + +@jit +def _init_grads_laplacian_Jastrow_three_body_streaming_state( + jastrow_three_body_data: Jastrow_three_body_data, + r_up_carts: jax.Array, + r_dn_carts: jax.Array, +) -> Jastrow_three_body_streaming_state: + """Initialize the J3 streaming state from a configuration ``(r_up, r_dn)``. + + This is a one-shot evaluation equivalent in cost to + :func:`compute_grads_and_laplacian_Jastrow_three_body`; it additionally + materializes the auxiliary tables required by the rank-1 advance. + """ + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + orb_data = jastrow_three_body_data.orb_data + compute_orb, compute_orb_grad, compute_orb_lapl = _three_body_orb_apis(jastrow_three_body_data) + + # AO/MO tables (forward r_*_carts unchanged so the underlying kernels can + # reconstruct r-R in float64 — Principle 3b). + aos_up = jnp.asarray(compute_orb(orb_data, r_up_carts), dtype=dtype_jnp) + aos_dn = jnp.asarray(compute_orb(orb_data, r_dn_carts), dtype=dtype_jnp) + + grad_up_x, grad_up_y, grad_up_z = compute_orb_grad(orb_data, r_up_carts) + grad_dn_x, grad_dn_y, grad_dn_z = compute_orb_grad(orb_data, r_dn_carts) + grad_aos_up = jnp.asarray(jnp.stack([grad_up_x, grad_up_y, grad_up_z], axis=-1), dtype=dtype_jnp) + grad_aos_dn = jnp.asarray(jnp.stack([grad_dn_x, grad_dn_y, grad_dn_z], axis=-1), dtype=dtype_jnp) + + lap_aos_up = jnp.asarray(compute_orb_lapl(orb_data, r_up_carts), dtype=dtype_jnp) + lap_aos_dn = jnp.asarray(compute_orb_lapl(orb_data, r_dn_carts), dtype=dtype_jnp) + + j_matrix = jastrow_three_body_data._j_matrix_jnp.astype(dtype_jnp) + j1_vec = j_matrix[:, -1] + j3_mat = j_matrix[:, :-1] + + num_up = aos_up.shape[1] + num_dn = aos_dn.shape[1] + + j3_mat_aos_up = j3_mat @ aos_up + j3_mat_T_aos_up = j3_mat.T @ aos_up + j3_mat_aos_dn = j3_mat @ aos_dn + j3_mat_T_aos_dn = j3_mat.T @ aos_dn + + upper_up = jnp.triu(jnp.ones((num_up, num_up), dtype=dtype_jnp), k=1) + lower_up = jnp.tril(jnp.ones((num_up, num_up), dtype=dtype_jnp), k=-1) + upper_dn = jnp.triu(jnp.ones((num_dn, num_dn), dtype=dtype_jnp), k=1) + lower_dn = jnp.tril(jnp.ones((num_dn, num_dn), dtype=dtype_jnp), k=-1) + + g_up = ( + j1_vec[:, None] + + j3_mat_aos_up @ lower_up + + j3_mat_T_aos_up @ upper_up + + j3_mat_aos_dn @ jnp.ones((num_dn, 1), dtype=dtype_jnp) + ) + g_dn = ( + j1_vec[:, None] + + j3_mat_aos_dn @ lower_dn + + j3_mat_T_aos_dn @ upper_dn + + j3_mat_T_aos_up @ jnp.ones((num_up, 1), dtype=dtype_jnp) + ) + + grad_J3_up = jnp.einsum("on,onj->nj", g_up, grad_aos_up) + grad_J3_dn = jnp.einsum("on,onj->nj", g_dn, grad_aos_dn) + lap_J3_up = jnp.einsum("on,on->n", g_up, lap_aos_up) + lap_J3_dn = jnp.einsum("on,on->n", g_dn, lap_aos_dn) + + return Jastrow_three_body_streaming_state( + aos_up=aos_up, + aos_dn=aos_dn, + grad_aos_up=grad_aos_up, + grad_aos_dn=grad_aos_dn, + lap_aos_up=lap_aos_up, + lap_aos_dn=lap_aos_dn, + j3_mat_aos_up=j3_mat_aos_up, + j3_mat_aos_dn=j3_mat_aos_dn, + j3_mat_T_aos_up=j3_mat_T_aos_up, + j3_mat_T_aos_dn=j3_mat_T_aos_dn, + g_up=g_up, + g_dn=g_dn, + grad_J3_up=grad_J3_up, + grad_J3_dn=grad_J3_dn, + lap_J3_up=lap_J3_up, + lap_J3_dn=lap_J3_dn, + ) + + +@jit +def _advance_grads_laplacian_Jastrow_three_body_streaming_state( + jastrow_three_body_data: Jastrow_three_body_data, + state: Jastrow_three_body_streaming_state, + moved_spin_is_up: jax.Array, + moved_index: jax.Array, + r_up_carts_new: jax.Array, + r_dn_carts_new: jax.Array, +) -> Jastrow_three_body_streaming_state: + """Advance the J3 streaming state after a single-electron move. + + The new ``(r_up_carts_new, r_dn_carts_new)`` differ from the configuration + represented by ``state`` in *exactly one* electron position, identified by + ``(moved_spin_is_up, moved_index)``. If neither spin actually moved (e.g. a + no-op step), the state should still be passed through unchanged by the + caller — this routine assumes a real one-electron displacement. + + Cost: ``O(n_ao^2 + n_ao * N_e)`` per call, dominated by two ``n_ao``-sized + matvecs ``j3_mat @ delta_aos`` and one full einsum over ``g``. + """ + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + orb_data = jastrow_three_body_data.orb_data + compute_orb, compute_orb_grad, compute_orb_lapl = _three_body_orb_apis(jastrow_three_body_data) + + j_matrix = jastrow_three_body_data._j_matrix_jnp.astype(dtype_jnp) + j3_mat = j_matrix[:, :-1] + + num_up = state.aos_up.shape[1] + num_dn = state.aos_dn.shape[1] + + def _branch_up(_): + # Single-point AO eval at the moved electron's new position. + # NB: forward r_up_carts_new unchanged (Principle 3b — fp64 r-R + # reconstruction inside the kernels). + r_new = jnp.expand_dims(r_up_carts_new[moved_index], axis=0) # (1, 3) + aos_new_col = jnp.asarray(compute_orb(orb_data, r_new)[:, 0], dtype=dtype_jnp) + gx, gy, gz = compute_orb_grad(orb_data, r_new) + grad_aos_new_col = jnp.asarray(jnp.stack([gx[:, 0], gy[:, 0], gz[:, 0]], axis=-1), dtype=dtype_jnp) + lap_aos_new_col = jnp.asarray(compute_orb_lapl(orb_data, r_new)[:, 0], dtype=dtype_jnp) + + delta_aos = aos_new_col - state.aos_up[:, moved_index] + d_J = j3_mat @ delta_aos # (n_orb,) + d_JT = j3_mat.T @ delta_aos # (n_orb,) + + # Update auxiliary matmuls at the moved column. + new_j3_mat_aos_up = state.j3_mat_aos_up.at[:, moved_index].add(d_J) + new_j3_mat_T_aos_up = state.j3_mat_T_aos_up.at[:, moved_index].add(d_JT) + # j3_mat_aos_dn / j3_mat_T_aos_dn unchanged (depend on aos_dn). + + # g_up update: + # term A (j3_mat_aos_up @ lower_up): col j gets +d_J for j < k. + # term B (j3_mat_T_aos_up @ upper_up): col j gets +d_JT for j > k. + # term C (cross-spin via aos_dn): unchanged. + # col k itself: unchanged (strict triangulars set k-th col to 0). + col_idx_up = jnp.arange(num_up) + mask_lt = (col_idx_up < moved_index).astype(dtype_jnp) + mask_gt = (col_idx_up > moved_index).astype(dtype_jnp) + new_g_up = state.g_up + d_J[:, None] * mask_lt[None, :] + d_JT[:, None] * mask_gt[None, :] + + # g_dn update: term C is (j3_mat.T @ aos_up) @ ones_up, so the change + # is sum_k Δ(j3_mat.T @ aos_up)[:, k] = d_JT (single column changed). + # Same vector added to every dn column. + new_g_dn = state.g_dn + d_JT[:, None] + + # Update aos/grad_aos/lap_aos at the moved column. + new_aos_up = state.aos_up.at[:, moved_index].set(aos_new_col) + new_grad_aos_up = state.grad_aos_up.at[:, moved_index, :].set(grad_aos_new_col) + new_lap_aos_up = state.lap_aos_up.at[:, moved_index].set(lap_aos_new_col) + + # Recompute per-electron grad_J3_*, lap_J3_* via einsum on updated + # tables. Cost: O(n_ao * N_e * 3) — within target asymptotics. + grad_J3_up = jnp.einsum("on,onj->nj", new_g_up, new_grad_aos_up) + grad_J3_dn = jnp.einsum("on,onj->nj", new_g_dn, state.grad_aos_dn) + lap_J3_up = jnp.einsum("on,on->n", new_g_up, new_lap_aos_up) + lap_J3_dn = jnp.einsum("on,on->n", new_g_dn, state.lap_aos_dn) + + return state.replace( + aos_up=new_aos_up, + grad_aos_up=new_grad_aos_up, + lap_aos_up=new_lap_aos_up, + j3_mat_aos_up=new_j3_mat_aos_up, + j3_mat_T_aos_up=new_j3_mat_T_aos_up, + g_up=new_g_up, + g_dn=new_g_dn, + grad_J3_up=grad_J3_up, + grad_J3_dn=grad_J3_dn, + lap_J3_up=lap_J3_up, + lap_J3_dn=lap_J3_dn, + ) + + def _branch_dn(_): + r_new = jnp.expand_dims(r_dn_carts_new[moved_index], axis=0) + aos_new_col = jnp.asarray(compute_orb(orb_data, r_new)[:, 0], dtype=dtype_jnp) + gx, gy, gz = compute_orb_grad(orb_data, r_new) + grad_aos_new_col = jnp.asarray(jnp.stack([gx[:, 0], gy[:, 0], gz[:, 0]], axis=-1), dtype=dtype_jnp) + lap_aos_new_col = jnp.asarray(compute_orb_lapl(orb_data, r_new)[:, 0], dtype=dtype_jnp) + + delta_aos = aos_new_col - state.aos_dn[:, moved_index] + d_J = j3_mat @ delta_aos + d_JT = j3_mat.T @ delta_aos + + new_j3_mat_aos_dn = state.j3_mat_aos_dn.at[:, moved_index].add(d_J) + new_j3_mat_T_aos_dn = state.j3_mat_T_aos_dn.at[:, moved_index].add(d_JT) + + col_idx_dn = jnp.arange(num_dn) + mask_lt = (col_idx_dn < moved_index).astype(dtype_jnp) + mask_gt = (col_idx_dn > moved_index).astype(dtype_jnp) + new_g_dn = state.g_dn + d_J[:, None] * mask_lt[None, :] + d_JT[:, None] * mask_gt[None, :] + + # g_up term C is (j3_mat @ aos_dn) @ ones_dn, change = d_J for every up col. + new_g_up = state.g_up + d_J[:, None] + + new_aos_dn = state.aos_dn.at[:, moved_index].set(aos_new_col) + new_grad_aos_dn = state.grad_aos_dn.at[:, moved_index, :].set(grad_aos_new_col) + new_lap_aos_dn = state.lap_aos_dn.at[:, moved_index].set(lap_aos_new_col) + + grad_J3_up = jnp.einsum("on,onj->nj", new_g_up, state.grad_aos_up) + grad_J3_dn = jnp.einsum("on,onj->nj", new_g_dn, new_grad_aos_dn) + lap_J3_up = jnp.einsum("on,on->n", new_g_up, state.lap_aos_up) + lap_J3_dn = jnp.einsum("on,on->n", new_g_dn, new_lap_aos_dn) + + return state.replace( + aos_dn=new_aos_dn, + grad_aos_dn=new_grad_aos_dn, + lap_aos_dn=new_lap_aos_dn, + j3_mat_aos_dn=new_j3_mat_aos_dn, + j3_mat_T_aos_dn=new_j3_mat_T_aos_dn, + g_up=new_g_up, + g_dn=new_g_dn, + grad_J3_up=grad_J3_up, + grad_J3_dn=grad_J3_dn, + lap_J3_up=lap_J3_up, + lap_J3_dn=lap_J3_dn, + ) + + # Edge case: zero-electron spin sector — no advance possible, just no-op. + if num_up == 0: + return _branch_dn(None) + if num_dn == 0: + return _branch_up(None) + return jax.lax.cond(moved_spin_is_up, _branch_up, _branch_dn, operand=None) + + def _compute_grads_and_laplacian_Jastrow_three_body_debug( jastrow_three_body_data: Jastrow_three_body_data, r_up_carts: np.ndarray, diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index e9886e16..3000abbe 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -92,6 +92,10 @@ ) from .swct import evaluate_swct_domega, evaluate_swct_omega from .wavefunction import ( + Kinetic_streaming_state, + _advance_kinetic_energy_all_elements_streaming_state, + _init_kinetic_energy_all_elements_streaming_state, + _kinetic_energy_from_streaming_state, compute_discretized_kinetic_energy, compute_discretized_kinetic_energy_fast_update, compute_kinetic_energy_all_elements, @@ -4456,30 +4460,37 @@ def _projection_n( """ @jit - def _body_fun_n(i, carry): - ( - w_L, - r_up_carts, - r_dn_carts, - RT, - A_old_inv, - _, - _, - ) = carry + def _body_step_core( + i, + w_L, + r_up_carts, + r_dn_carts, + A_old_inv, + diagonal_kinetic_continuum_elements_up, + diagonal_kinetic_continuum_elements_dn, + j3_state=None, + ): + """Single GFMC projection step, parameterized by per-electron continuum kinetic energy. + + Extracted from the original ``_body_fun_n`` so that both the legacy path + (which recomputes ``compute_kinetic_energy_all_elements_fast_update`` from + scratch every step) and the streaming path (which reads the kinetic + energy from a maintained ``Kinetic_streaming_state``) can share the + identical post-kinetic logic. Returns the carry components plus the + ``(moved_spin_is_up, moved_index)`` pair that the streaming path needs + to advance its auxiliary state. + + The optional ``j3_state`` (a ``Jastrow_three_body_streaming_state`` + consistent with the current ``r_{up,dn}_carts``) lets the streaming + path avoid re-evaluating J3 AOs / W,U / cross-vec auxiliaries inside + the discretized-mesh kinetic ratio and the ECP non-local ratio. Pass + ``None`` (default) on the legacy path; the callees fall back to fresh + AO evaluation when ``j3_state is None``. + """ # compute diagonal elements, kinetic part diagonal_kinetic_part = 3.0 / (2.0 * alat**2) * (len(r_up_carts) + len(r_dn_carts)) - # compute continuum kinetic energy - diagonal_kinetic_continuum_elements_up, diagonal_kinetic_continuum_elements_dn = ( - compute_kinetic_energy_all_elements_fast_update( - wavefunction_data=hamiltonian_data.wavefunction_data, - r_up_carts=r_up_carts, - r_dn_carts=r_dn_carts, - geminal_inverse=A_old_inv, - ) - ) - # generate a random rotation matrix rot_key = rotation_keys[i] if random_discretized_mesh: @@ -4499,6 +4510,7 @@ def _body_fun_n(i, carry): r_up_carts=r_up_carts, r_dn_carts=r_dn_carts, RT=R.T, + j3_state=j3_state, ) ) # spin-filp @@ -4632,6 +4644,7 @@ def _body_fun_n(i, carry): flag_determinant_only=False, A_old_inv=A_old_inv, RT=R.T, + j3_state=j3_state, ) ) @@ -4658,6 +4671,7 @@ def _body_fun_n(i, carry): flag_determinant_only=True, A_old_inv=A_old_inv, RT=R.T, + j3_state=j3_state, ) ) @@ -4670,6 +4684,7 @@ def _body_fun_n(i, carry): old_r_dn_carts=r_dn_carts, new_r_up_carts_arr=mesh_non_local_ecp_part_r_up_carts, new_r_dn_carts_arr=mesh_non_local_ecp_part_r_dn_carts, + j3_state=j3_state, ) V_nonlocal_FN = V_nonlocal_FN * Jastrow_ratio @@ -4812,7 +4827,17 @@ def _update_inv_dn_n(_): r_up_carts = proposed_r_up_carts r_dn_carts = proposed_r_dn_carts - carry = ( + # ``moved_index`` is whichever of (up_index, dn_index) is the + # one whose spin actually moved this step. Used by the streaming + # path to identify the column-changed by the rank-1 update. + # If neither moved (no_move case), ``moved_spin_is_up`` and + # ``moved_index`` are still well-defined arrays and the + # streaming advance handles the no-op robustly via + # ``r_up_carts`` / ``r_dn_carts`` which haven't actually changed. + moved_spin_is_up = has_up_move + moved_index = jnp.where(has_up_move, up_index, dn_index) + + return ( w_L, r_up_carts, r_dn_carts, @@ -4820,8 +4845,116 @@ def _update_inv_dn_n(_): A_new_inv, diagonal_sum_hamiltonian, non_diagonal_sum_hamiltonian, + moved_spin_is_up, + moved_index, + ) + + @jit + def _body_fun_n(i, carry): + """Legacy GFMC projection body — recomputes kinetic energies fresh per step.""" + ( + w_L, + r_up_carts, + r_dn_carts, + RT, + A_old_inv, + _, + _, + ) = carry + + # compute continuum kinetic energy from scratch (legacy path). + ke_up, ke_dn = compute_kinetic_energy_all_elements_fast_update( + wavefunction_data=hamiltonian_data.wavefunction_data, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, + geminal_inverse=A_old_inv, + ) + + ( + w_L_new, + r_up_new, + r_dn_new, + RT_new, + A_new_inv, + diag_sum_H, + nondiag_sum_H, + _moved_spin_is_up, + _moved_index, + ) = _body_step_core(i, w_L, r_up_carts, r_dn_carts, A_old_inv, ke_up, ke_dn) + + return ( + w_L_new, + r_up_new, + r_dn_new, + RT_new, + A_new_inv, + diag_sum_H, + nondiag_sum_H, + ) + + @jit + def _body_fun_n_streaming(i, carry): + """Streaming GFMC projection body — reads kinetic energies from a maintained + ``Kinetic_streaming_state`` (J3 incrementally; J1/J2/det fresh in PR1) and + advances the state at the end of each step. + + Only valid when ``jastrow_nn_data is None`` (NN J3 has no streaming path). + Dispatch is Python-static at the ``_projection_n`` entry point. + """ + ( + w_L, + r_up_carts, + r_dn_carts, + RT, + A_old_inv, + kinetic_state, + _, + _, + ) = carry + + ke_up, ke_dn = _kinetic_energy_from_streaming_state(kinetic_state) + + ( + w_L_new, + r_up_new, + r_dn_new, + RT_new, + A_new_inv, + diag_sum_H, + nondiag_sum_H, + moved_spin_is_up, + moved_index, + ) = _body_step_core( + i, + w_L, + r_up_carts, + r_dn_carts, + A_old_inv, + ke_up, + ke_dn, + j3_state=kinetic_state.j3_state, + ) + + kinetic_state_new = _advance_kinetic_energy_all_elements_streaming_state( + wavefunction_data=hamiltonian_data.wavefunction_data, + state=kinetic_state, + moved_spin_is_up=moved_spin_is_up, + moved_index=moved_index, + r_up_carts_new=r_up_new, + r_dn_carts_new=r_dn_new, + A_new_inv=A_new_inv, + ) + + return ( + w_L_new, + r_up_new, + r_dn_new, + RT_new, + A_new_inv, + kinetic_state_new, + diag_sum_H, + nondiag_sum_H, ) - return carry def _split_step_keys(key, num_steps): def _split_body(current_key, _): @@ -4833,28 +4966,69 @@ def _split_body(current_key, _): latest_jax_PRNG_key, (rotation_keys, move_keys) = _split_step_keys(init_jax_PRNG_key, num_mcmc_per_measurement) - ( - latest_w_L, - latest_r_up_carts, - latest_r_dn_carts, - latest_RT, - latest_A_old_inv, - latest_diagonal_sum_hamiltonian, - latest_non_diagonal_sum_hamiltonian, - ) = jax.lax.fori_loop( - 0, - num_mcmc_per_measurement, - _body_fun_n, + # Python-static dispatch: the streaming path is incompatible with + # the NN three-body Jastrow (J_NN has no rank-1 advance — see + # lrdmc_refactoring.md 1-4). When NN J3 is present, fall back to + # the legacy path that recomputes kinetic energies fresh each step. + # The streaming path is also compatible only when J3 is present; + # otherwise the gain over legacy is zero, so we still use legacy. + jastrow_data = hamiltonian_data.wavefunction_data.jastrow_data + use_streaming = jastrow_data.jastrow_nn_data is None and jastrow_data.jastrow_three_body_data is not None + + if use_streaming: + init_kinetic_state = _init_kinetic_energy_all_elements_streaming_state( + wavefunction_data=hamiltonian_data.wavefunction_data, + r_up_carts=init_r_up_carts, + r_dn_carts=init_r_dn_carts, + geminal_inverse=init_A_old_inv, + ) ( - init_w_L, - init_r_up_carts, - init_r_dn_carts, - jnp.eye(3, dtype=jnp.float64), - init_A_old_inv, - jnp.asarray(0.0, dtype=jnp.float64), - jnp.asarray(0.0, dtype=jnp.float64), - ), - ) + latest_w_L, + latest_r_up_carts, + latest_r_dn_carts, + latest_RT, + latest_A_old_inv, + _latest_kinetic_state, + latest_diagonal_sum_hamiltonian, + latest_non_diagonal_sum_hamiltonian, + ) = jax.lax.fori_loop( + 0, + num_mcmc_per_measurement, + _body_fun_n_streaming, + ( + init_w_L, + init_r_up_carts, + init_r_dn_carts, + jnp.eye(3, dtype=jnp.float64), + init_A_old_inv, + init_kinetic_state, + jnp.asarray(0.0, dtype=jnp.float64), + jnp.asarray(0.0, dtype=jnp.float64), + ), + ) + else: + ( + latest_w_L, + latest_r_up_carts, + latest_r_dn_carts, + latest_RT, + latest_A_old_inv, + latest_diagonal_sum_hamiltonian, + latest_non_diagonal_sum_hamiltonian, + ) = jax.lax.fori_loop( + 0, + num_mcmc_per_measurement, + _body_fun_n, + ( + init_w_L, + init_r_up_carts, + init_r_dn_carts, + jnp.eye(3, dtype=jnp.float64), + init_A_old_inv, + jnp.asarray(0.0, dtype=jnp.float64), + jnp.asarray(0.0, dtype=jnp.float64), + ), + ) return ( latest_w_L, diff --git a/jqmc/wavefunction.py b/jqmc/wavefunction.py index 60e2076a..11f7c0cd 100644 --- a/jqmc/wavefunction.py +++ b/jqmc/wavefunction.py @@ -57,8 +57,11 @@ from ._precision import get_dtype_jnp from .atomic_orbital import AOs_cart_data, AOs_sphe_data, ShellPrimMap from .determinant import ( + Det_streaming_state, Geminal_data, + _advance_grads_laplacian_ln_Det_streaming_state, _compute_ratio_determinant_part_split_spin, + _init_grads_laplacian_ln_Det_streaming_state, compute_det_geminal_all_elements, compute_grads_and_laplacian_ln_Det, compute_grads_and_laplacian_ln_Det_fast, @@ -67,8 +70,19 @@ ) from .jastrow_factor import ( Jastrow_data, + Jastrow_one_body_streaming_state, + Jastrow_three_body_streaming_state, + Jastrow_two_body_streaming_state, + _advance_grads_laplacian_Jastrow_one_body_streaming_state, + _advance_grads_laplacian_Jastrow_three_body_streaming_state, + _advance_grads_laplacian_Jastrow_two_body_streaming_state, _compute_ratio_Jastrow_part_rank1_update, + _init_grads_laplacian_Jastrow_one_body_streaming_state, + _init_grads_laplacian_Jastrow_three_body_streaming_state, + _init_grads_laplacian_Jastrow_two_body_streaming_state, + compute_grads_and_laplacian_Jastrow_one_body, compute_grads_and_laplacian_Jastrow_part, + compute_grads_and_laplacian_Jastrow_two_body, compute_Jastrow_part, ) from .molecular_orbital import MOs_data @@ -1206,6 +1220,303 @@ def _compute_kinetic_energy_all_elements_fast_update_debug( ) +# --------------------------------------------------------------------------- +# Per-electron kinetic-energy streaming state (used by GFMC projection) +# --------------------------------------------------------------------------- +# +# Maintains enough auxiliary information to advance the per-electron kinetic +# energies after a single-electron move without recomputing them from +# scratch. PR1 (devel-speedup-lrdmc-incremental) enables this only for the +# J3 part — J1, J2 and the determinant gradients/Laplacians are still +# recomputed fresh inside ``_advance_*``. Subsequent PRs will replace those +# fresh recomputes with rank-1 updates while keeping the public per-electron +# fields (``grad_J_up`` etc.) shape-stable. +# +# The state is freshly built at every branching boundary by +# ``_init_kinetic_energy_all_elements_streaming_state`` (lifetime matches +# the Sherman-Morrison ``A_old_inv``). + + +@struct.dataclass +class Kinetic_streaming_state: + """Streaming state for per-electron kinetic-energy evaluation. + + Fields evaluated at the current ``(r_up_carts, r_dn_carts)``: + + - ``j3_state``: J3 auxiliary tables (None if no J3 component is active). + - ``det_state``: det auxiliary tables (always populated in PR2+; the + determinant per-electron grad/lap fields below mirror its outputs). + - ``grad_J_up`` / ``grad_J_dn``: total Jastrow per-electron gradient. + - ``lap_J_up`` / ``lap_J_dn``: total Jastrow per-electron Laplacian. + - ``grad_ln_D_up`` / ``grad_ln_D_dn``: per-electron ``∇ln|Det|`` from the + geminal at the current ``A_old_inv``. + - ``lap_ln_D_up`` / ``lap_ln_D_dn``: per-electron ``∇²ln|Det|``. + """ + + j1_state: Jastrow_one_body_streaming_state | None = struct.field(pytree_node=True, default=None) + j2_state: Jastrow_two_body_streaming_state | None = struct.field(pytree_node=True, default=None) + j3_state: Jastrow_three_body_streaming_state | None = struct.field(pytree_node=True, default=None) + det_state: Det_streaming_state | None = struct.field(pytree_node=True, default=None) + grad_J_up: jax.Array = struct.field(pytree_node=True, default=None) + grad_J_dn: jax.Array = struct.field(pytree_node=True, default=None) + lap_J_up: jax.Array = struct.field(pytree_node=True, default=None) + lap_J_dn: jax.Array = struct.field(pytree_node=True, default=None) + grad_ln_D_up: jax.Array = struct.field(pytree_node=True, default=None) + grad_ln_D_dn: jax.Array = struct.field(pytree_node=True, default=None) + lap_ln_D_up: jax.Array = struct.field(pytree_node=True, default=None) + lap_ln_D_dn: jax.Array = struct.field(pytree_node=True, default=None) + + +def _kinetic_energy_from_grads_laps( + grad_J_up, + grad_J_dn, + lap_J_up, + lap_J_dn, + grad_ln_D_up, + grad_ln_D_dn, + lap_ln_D_up, + lap_ln_D_dn, +): + """Common assembly: ``-(1/2) * (∇²ln Ψ + ||∇ln Ψ||²)`` per electron.""" + dtype_jnp = get_dtype_jnp("wf_kinetic") + grad_J_up = jnp.asarray(grad_J_up, dtype=dtype_jnp) + grad_J_dn = jnp.asarray(grad_J_dn, dtype=dtype_jnp) + lap_J_up = jnp.asarray(lap_J_up, dtype=dtype_jnp) + lap_J_dn = jnp.asarray(lap_J_dn, dtype=dtype_jnp) + grad_ln_D_up = jnp.asarray(grad_ln_D_up, dtype=dtype_jnp) + grad_ln_D_dn = jnp.asarray(grad_ln_D_dn, dtype=dtype_jnp) + lap_ln_D_up = jnp.asarray(lap_ln_D_up, dtype=dtype_jnp) + lap_ln_D_dn = jnp.asarray(lap_ln_D_dn, dtype=dtype_jnp) + + grad_ln_Psi_up = grad_J_up + grad_ln_D_up + grad_ln_Psi_dn = grad_J_dn + grad_ln_D_dn + lap_ln_Psi_up = lap_J_up + lap_ln_D_up + lap_ln_Psi_dn = lap_J_dn + lap_ln_D_dn + ke_up = -0.5 * (lap_ln_Psi_up + jnp.sum(grad_ln_Psi_up**2, axis=1)) + ke_dn = -0.5 * (lap_ln_Psi_dn + jnp.sum(grad_ln_Psi_dn**2, axis=1)) + return ke_up, ke_dn + + +def _init_kinetic_energy_all_elements_streaming_state( + wavefunction_data: Wavefunction_data, + r_up_carts: jax.Array, + r_dn_carts: jax.Array, + geminal_inverse: jax.Array, +) -> Kinetic_streaming_state: + """Build a fresh streaming state at the supplied ``(r_up, r_dn)``. + + PR1+PR2+PR3 scope: J1, J2, J3, and det sub-states are all incrementally + maintained. NN three-body falls back to ``compute_grads_and_laplacian_Jastrow_part`` + (the streaming dispatch in ``jqmc_gfmc.py`` already excludes the NN case). + + Note: ``geminal_inverse`` must be the inverse of ``G(r_up, r_dn)`` (the + same invariant as :func:`compute_kinetic_energy_all_elements_fast_update`). + """ + # Per-electron Jastrow grad/lap (sum of J1/J2/J3/NN parts) — used as the + # initial total. Sub-states below are populated for the streaming path. + grad_J_up, grad_J_dn, lap_J_up, lap_J_dn = compute_grads_and_laplacian_Jastrow_part( + jastrow_data=wavefunction_data.jastrow_data, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, + ) + + # Determinant streaming state — drives grad_ln_D_*/lap_ln_D_* fields. + det_state = _init_grads_laplacian_ln_Det_streaming_state( + geminal_data=wavefunction_data.geminal_data, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, + geminal_inverse=geminal_inverse, + ) + + jastrow_data = wavefunction_data.jastrow_data + j1_data = jastrow_data.jastrow_one_body_data + j2_data = jastrow_data.jastrow_two_body_data + j3_data = jastrow_data.jastrow_three_body_data + j1_state = ( + _init_grads_laplacian_Jastrow_one_body_streaming_state(j1_data, r_up_carts, r_dn_carts) if j1_data is not None else None + ) + j2_state = ( + _init_grads_laplacian_Jastrow_two_body_streaming_state(j2_data, r_up_carts, r_dn_carts) if j2_data is not None else None + ) + j3_state = ( + _init_grads_laplacian_Jastrow_three_body_streaming_state(j3_data, r_up_carts, r_dn_carts) + if j3_data is not None + else None + ) + + return Kinetic_streaming_state( + j1_state=j1_state, + j2_state=j2_state, + j3_state=j3_state, + det_state=det_state, + grad_J_up=grad_J_up, + grad_J_dn=grad_J_dn, + lap_J_up=lap_J_up, + lap_J_dn=lap_J_dn, + grad_ln_D_up=det_state.grad_ln_D_up, + grad_ln_D_dn=det_state.grad_ln_D_dn, + lap_ln_D_up=det_state.lap_ln_D_up, + lap_ln_D_dn=det_state.lap_ln_D_dn, + ) + + +def _kinetic_energy_from_streaming_state(state: Kinetic_streaming_state): + """Per-electron kinetic energies extracted from a streaming state.""" + return _kinetic_energy_from_grads_laps( + state.grad_J_up, + state.grad_J_dn, + state.lap_J_up, + state.lap_J_dn, + state.grad_ln_D_up, + state.grad_ln_D_dn, + state.lap_ln_D_up, + state.lap_ln_D_dn, + ) + + +def _advance_kinetic_energy_all_elements_streaming_state( + wavefunction_data: Wavefunction_data, + state: Kinetic_streaming_state, + moved_spin_is_up: jax.Array, + moved_index: jax.Array, + r_up_carts_new: jax.Array, + r_dn_carts_new: jax.Array, + A_new_inv: jax.Array, +) -> Kinetic_streaming_state: + """Advance the streaming state after a single-electron move. + + PR1+PR2+PR3 scope: J1, J2, J3, and det sub-states are all updated + incrementally. NN three-body falls back to a fresh + ``compute_grads_and_laplacian_Jastrow_part`` call (defensive — the + streaming dispatch in ``jqmc_gfmc.py`` excludes the NN case so this + branch is unreachable in production). + + The returned state is consistent with ``(r_up_carts_new, r_dn_carts_new, + A_new_inv)``; downstream consumers can read kinetic energies via + :func:`_kinetic_energy_from_streaming_state`. + """ + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + jastrow_data = wavefunction_data.jastrow_data + + # --- J1: incremental advance via streaming state --------------------- + j1_data = jastrow_data.jastrow_one_body_data + if j1_data is not None and state.j1_state is not None: + new_j1_state = _advance_grads_laplacian_Jastrow_one_body_streaming_state( + j1_data, + state.j1_state, + moved_spin_is_up, + moved_index, + r_up_carts_new, + r_dn_carts_new, + ) + grad_J1_up = new_j1_state.grad_J1_up + grad_J1_dn = new_j1_state.grad_J1_dn + lap_J1_up = new_j1_state.lap_J1_up + lap_J1_dn = new_j1_state.lap_J1_dn + else: + new_j1_state = None + grad_J1_up = jnp.zeros_like(state.grad_J_up) + grad_J1_dn = jnp.zeros_like(state.grad_J_dn) + lap_J1_up = jnp.zeros_like(state.lap_J_up) + lap_J1_dn = jnp.zeros_like(state.lap_J_dn) + + # --- J2: incremental advance via streaming state --------------------- + j2_data = jastrow_data.jastrow_two_body_data + if j2_data is not None and state.j2_state is not None: + new_j2_state = _advance_grads_laplacian_Jastrow_two_body_streaming_state( + j2_data, + state.j2_state, + moved_spin_is_up, + moved_index, + r_up_carts_new, + r_dn_carts_new, + ) + grad_J2_up = new_j2_state.grad_J2_up + grad_J2_dn = new_j2_state.grad_J2_dn + lap_J2_up = new_j2_state.lap_J2_up + lap_J2_dn = new_j2_state.lap_J2_dn + else: + new_j2_state = None + grad_J2_up = jnp.zeros_like(state.grad_J_up) + grad_J2_dn = jnp.zeros_like(state.grad_J_dn) + lap_J2_up = jnp.zeros_like(state.lap_J_up) + lap_J2_dn = jnp.zeros_like(state.lap_J_dn) + + # --- J3: incremental advance via streaming state --------------------- + j3_data = jastrow_data.jastrow_three_body_data + if j3_data is not None and state.j3_state is not None: + new_j3_state = _advance_grads_laplacian_Jastrow_three_body_streaming_state( + j3_data, + state.j3_state, + moved_spin_is_up, + moved_index, + r_up_carts_new, + r_dn_carts_new, + ) + grad_J3_up = new_j3_state.grad_J3_up + grad_J3_dn = new_j3_state.grad_J3_dn + lap_J3_up = new_j3_state.lap_J3_up + lap_J3_dn = new_j3_state.lap_J3_dn + else: + new_j3_state = None + grad_J3_up = jnp.zeros_like(state.grad_J_up) + grad_J3_dn = jnp.zeros_like(state.grad_J_dn) + lap_J3_up = jnp.zeros_like(state.lap_J_up) + lap_J3_dn = jnp.zeros_like(state.lap_J_dn) + + # Reassemble Jastrow totals from the streamed sub-state contributions. + grad_J_up = grad_J1_up + grad_J2_up + grad_J3_up + grad_J_dn = grad_J1_dn + grad_J2_dn + grad_J3_dn + lap_J_up = lap_J1_up + lap_J2_up + lap_J3_up + lap_J_dn = lap_J1_dn + lap_J2_dn + lap_J3_dn + + # NN three-body (autodiff path) — defensive fallback. The streaming + # dispatch in ``jqmc_gfmc.py`` already routes NN-on cases to the legacy + # body, so this branch is unreachable in production. + if jastrow_data.jastrow_nn_data is not None: + grad_J_up_full, grad_J_dn_full, lap_J_up_full, lap_J_dn_full = compute_grads_and_laplacian_Jastrow_part( + jastrow_data=jastrow_data, + r_up_carts=r_up_carts_new, + r_dn_carts=r_dn_carts_new, + ) + grad_J_up = grad_J_up_full + grad_J_dn = grad_J_dn_full + lap_J_up = lap_J_up_full + lap_J_dn = lap_J_dn_full + + # --- determinant: incremental advance via streaming state ------------ + new_det_state = _advance_grads_laplacian_ln_Det_streaming_state( + geminal_data=wavefunction_data.geminal_data, + state=state.det_state, + moved_spin_is_up=moved_spin_is_up, + moved_index=moved_index, + r_up_carts_new=r_up_carts_new, + r_dn_carts_new=r_dn_carts_new, + A_new_inv=A_new_inv, + ) + + # Cast totals to jastrow_grad_lap dtype to match init's storage zone. + grad_J_up = jnp.asarray(grad_J_up, dtype=dtype_jnp) + grad_J_dn = jnp.asarray(grad_J_dn, dtype=dtype_jnp) + lap_J_up = jnp.asarray(lap_J_up, dtype=dtype_jnp) + lap_J_dn = jnp.asarray(lap_J_dn, dtype=dtype_jnp) + + return state.replace( + j1_state=new_j1_state, + j2_state=new_j2_state, + j3_state=new_j3_state, + det_state=new_det_state, + grad_J_up=grad_J_up, + grad_J_dn=grad_J_dn, + lap_J_up=lap_J_up, + lap_J_dn=lap_J_dn, + grad_ln_D_up=new_det_state.grad_ln_D_up, + grad_ln_D_dn=new_det_state.grad_ln_D_dn, + lap_ln_D_up=new_det_state.lap_ln_D_up, + lap_ln_D_dn=new_det_state.lap_ln_D_dn, + ) + + def _compute_discretized_kinetic_energy_debug( alat: float, wavefunction_data: Wavefunction_data, r_up_carts: npt.NDArray, r_dn_carts: npt.NDArray ) -> list[tuple[npt.NDArray, npt.NDArray]]: @@ -1422,6 +1733,7 @@ def compute_discretized_kinetic_energy_fast_update( r_up_carts: jax.Array, r_dn_carts: jax.Array, RT: jax.Array, + j3_state: "Jastrow_three_body_streaming_state | None" = None, ) -> tuple[jax.Array, jax.Array, jax.Array]: r"""Fast-update version of discretized kinetic mesh and ratios. @@ -1437,6 +1749,12 @@ def compute_discretized_kinetic_energy_fast_update( r_up_carts: Up-electron positions with shape ``(n_up, 3)``. r_dn_carts: Down-electron positions with shape ``(n_dn, 3)``. RT: Rotation matrix (:math:`R^T`) with shape ``(3, 3)``. + j3_state: Optional cached J3 streaming auxiliaries consistent with + ``(r_up_carts, r_dn_carts)``. Forwarded to the Jastrow ratio kernel + so it can skip the per-call ``aos_*_old``/``W``/``U``/cross_vec + recomputation. Use the value carried in the projection's + ``Kinetic_streaming_state.j3_state``; pass ``None`` (default) for + the original 1-shot path used by observation/MCMC code. Returns: Tuple ``(r_up_carts_combined, r_dn_carts_combined, elements_kinetic_part)`` with combined @@ -1518,6 +1836,7 @@ def compute_discretized_kinetic_energy_fast_update( old_r_dn_carts=r_dn, new_r_up_carts_arr=r_up_carts_combined, new_r_dn_carts_arr=r_dn_carts_combined, + j3_state=j3_state, ), dtype=dtype_wf_ratio_jnp, ) diff --git a/tests/test_determinant.py b/tests/test_determinant.py index 0b84ac86..a35b2d0c 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -50,6 +50,7 @@ from jqmc.atomic_orbital import AOs_sphe_data, compute_overlap_matrix # noqa: E402 from jqmc.determinant import ( # noqa: E402 Geminal_data, + _advance_grads_laplacian_ln_Det_streaming_state, _compute_AS_regularization_factor_debug, _compute_det_geminal_all_elements_debug, _compute_geminal_all_elements, @@ -59,6 +60,7 @@ _compute_grads_and_laplacian_ln_Det_fast_debug, _compute_ratio_determinant_part_debug, _compute_ratio_determinant_part_rank1_update, + _init_grads_laplacian_ln_Det_streaming_state, compute_AS_regularization_factor, compute_det_geminal_all_elements, compute_geminal_all_elements, @@ -1738,6 +1740,100 @@ def _make_aos(num_prim, seed): np.testing.assert_array_equal(grads["lambda_basis_coeff"][:, num_prim_up:], grad_coeff_dn) +# --------------------------------------------------------------------------- +# Det streaming state (PR2) +# --------------------------------------------------------------------------- + + +def _build_geminal_inverse(geminal_data, r_up_carts, r_dn_carts): + """Compute G(r_up, r_dn)^{-1} for a freshly-arrived configuration.""" + A = compute_geminal_all_elements( + geminal_data=geminal_data, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, + ) + return jnp.linalg.inv(A) + + +@pytest.mark.parametrize( + "trexio_file", + ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"], +) +def test_streaming_det_state_against_full(trexio_file: str): + """Det streaming state, after K random single-electron moves, must + reproduce ``compute_grads_and_laplacian_ln_Det_fast`` at the resulting + configuration.""" + ( + _, + _, + _, + _, + geminal_mo_data, + _, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + + n_up = geminal_mo_data.num_electron_up + n_dn = geminal_mo_data.num_electron_dn + + rng = np.random.RandomState(13) + r_up = jnp.asarray(4.0 * rng.rand(n_up, 3) - 2.0) + r_dn = jnp.asarray(4.0 * rng.rand(n_dn, 3) - 2.0) + + A_inv = _build_geminal_inverse(geminal_mo_data, r_up, r_dn) + state = _init_grads_laplacian_ln_Det_streaming_state( + geminal_data=geminal_mo_data, + r_up_carts=r_up, + r_dn_carts=r_dn, + geminal_inverse=A_inv, + ) + + K = 32 + for k in range(K): + # alternate spin choices, but skip the spin if it has 0 electrons + if n_dn == 0: + spin_is_up = True + elif n_up == 0: + spin_is_up = False + else: + spin_is_up = bool(rng.randint(0, 2)) + + if spin_is_up: + idx = int(rng.randint(0, n_up)) + r_up = r_up.at[idx].set(jnp.asarray(rng.normal(size=(3,)) * 0.4 + np.asarray(r_up[idx]))) + else: + idx = int(rng.randint(0, n_dn)) + r_dn = r_dn.at[idx].set(jnp.asarray(rng.normal(size=(3,)) * 0.4 + np.asarray(r_dn[idx]))) + + A_new_inv = _build_geminal_inverse(geminal_mo_data, r_up, r_dn) + state = _advance_grads_laplacian_ln_Det_streaming_state( + geminal_data=geminal_mo_data, + state=state, + moved_spin_is_up=jnp.bool_(spin_is_up), + moved_index=jnp.int32(idx), + r_up_carts_new=r_up, + r_dn_carts_new=r_dn, + A_new_inv=A_new_inv, + ) + + # Reference: fresh fast call at the final configuration. + A_inv_final = _build_geminal_inverse(geminal_mo_data, r_up, r_dn) + grad_up_ref, grad_dn_ref, lap_up_ref, lap_dn_ref = compute_grads_and_laplacian_ln_Det_fast( + geminal_data=geminal_mo_data, + r_up_carts=r_up, + r_dn_carts=r_dn, + geminal_inverse=A_inv_final, + ) + + atol, rtol = get_tolerance("det_grad_lap", "strict") + np.testing.assert_allclose(state.grad_ln_D_up, grad_up_ref, atol=atol, rtol=rtol) + np.testing.assert_allclose(state.grad_ln_D_dn, grad_dn_ref, atol=atol, rtol=rtol) + np.testing.assert_allclose(state.lap_ln_D_up, lap_up_ref, atol=atol, rtol=rtol) + np.testing.assert_allclose(state.lap_ln_D_dn, lap_dn_ref, atol=atol, rtol=rtol) + + if __name__ == "__main__": from logging import Formatter, StreamHandler, getLogger diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index 8fb286fb..067132df 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -1576,6 +1576,239 @@ def test_apply_block_update_nonsymmetric_j3_free(): np.testing.assert_allclose(new_j3, updated_values) +@pytest.mark.parametrize("j1b_type", ["exp", "pade"]) +@pytest.mark.parametrize("n_up,n_dn", [(5, 4), (1, 0), (3, 3)]) +def test_streaming_J1_state_against_full(j1b_type, n_up, n_dn): + """K random single-electron moves advanced via the J1 streaming state must + match a fresh init at the resulting configuration (and the existing analytic + full computation) within strict tolerance. + + J1 is per-electron independent (no electron-electron coupling), so the + advance only re-evaluates one row of the cached arrays. + """ + from jqmc.jastrow_factor import ( + _advance_grads_laplacian_Jastrow_one_body_streaming_state, + _init_grads_laplacian_Jastrow_one_body_streaming_state, + ) + + rng = np.random.RandomState(0) + num_R_cart_samples = 5 + R_carts = 4.0 * rng.rand(num_R_cart_samples, 3) - 2.0 + structure_data = Structure_data( + pbc_flag=False, + positions=R_carts, + atomic_numbers=tuple([6] * num_R_cart_samples), + element_symbols=tuple(["X"] * num_R_cart_samples), + atomic_labels=tuple(["X"] * num_R_cart_samples), + ) + core_electrons = tuple([2] * num_R_cart_samples) + + jastrow_one_body_data = Jastrow_one_body_data( + jastrow_1b_param=1.0, + jastrow_1b_type=j1b_type, + structure_data=structure_data, + core_electrons=core_electrons, + ) + + r_up = (4.0 * rng.rand(n_up, 3) - 2.0) if n_up > 0 else np.zeros((0, 3)) + r_dn = (4.0 * rng.rand(n_dn, 3) - 2.0) if n_dn > 0 else np.zeros((0, 3)) + + state = _init_grads_laplacian_Jastrow_one_body_streaming_state( + jastrow_one_body_data, jax.numpy.asarray(r_up), jax.numpy.asarray(r_dn) + ) + + K = 32 + atol, rtol = get_tolerance("wf_kinetic", "strict") + for _ in range(K): + spin_choices = [] + if n_up > 0: + spin_choices.append(0) + if n_dn > 0: + spin_choices.append(1) + spin = spin_choices[rng.randint(0, len(spin_choices))] + if spin == 0: + idx = rng.randint(0, n_up) + r_up = np.asarray(r_up).copy() + r_up[idx] = r_up[idx] + 0.1 * rng.randn(3) + moved_spin_is_up = True + moved_index = idx + else: + idx = rng.randint(0, n_dn) + r_dn = np.asarray(r_dn).copy() + r_dn[idx] = r_dn[idx] + 0.1 * rng.randn(3) + moved_spin_is_up = False + moved_index = idx + + state = _advance_grads_laplacian_Jastrow_one_body_streaming_state( + jastrow_one_body_data, + state, + jax.numpy.asarray(moved_spin_is_up), + jax.numpy.asarray(moved_index, dtype=jax.numpy.int32), + jax.numpy.asarray(r_up), + jax.numpy.asarray(r_dn), + ) + + g_up_full, g_dn_full, l_up_full, l_dn_full = compute_grads_and_laplacian_Jastrow_one_body( + jastrow_one_body_data, jax.numpy.asarray(r_up), jax.numpy.asarray(r_dn) + ) + np.testing.assert_allclose(np.asarray(state.grad_J1_up), np.asarray(g_up_full), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.grad_J1_dn), np.asarray(g_dn_full), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.lap_J1_up), np.asarray(l_up_full), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.lap_J1_dn), np.asarray(l_dn_full), atol=atol, rtol=rtol) + + +@pytest.mark.parametrize("j2b_type", ["pade", "exp"]) +@pytest.mark.parametrize("n_up,n_dn", [(5, 4), (1, 0), (3, 3)]) +def test_streaming_J2_state_against_full(j2b_type, n_up, n_dn): + """K random single-electron moves advanced via the J2 streaming state must + match a fresh init at the resulting configuration (and the existing analytic + full computation) within strict tolerance. + + J2 is electron-pair coupled, so the advance updates the moved electron's + same-spin row (i ≠ k) and the cross-spin partners. Sign asymmetry between + σ=up and σ=dn cross branches makes this the most error-prone of the + streaming kernels — exercise both branches with K=32 alternating moves. + """ + from jqmc.jastrow_factor import ( + _advance_grads_laplacian_Jastrow_two_body_streaming_state, + _init_grads_laplacian_Jastrow_two_body_streaming_state, + ) + + rng = np.random.RandomState(0) + jastrow_two_body_data = Jastrow_two_body_data(jastrow_2b_param=1.0, jastrow_2b_type=j2b_type) + + r_up = (4.0 * rng.rand(n_up, 3) - 2.0) if n_up > 0 else np.zeros((0, 3)) + r_dn = (4.0 * rng.rand(n_dn, 3) - 2.0) if n_dn > 0 else np.zeros((0, 3)) + + state = _init_grads_laplacian_Jastrow_two_body_streaming_state( + jastrow_two_body_data, jax.numpy.asarray(r_up), jax.numpy.asarray(r_dn) + ) + + K = 32 + atol, rtol = get_tolerance("wf_kinetic", "strict") + for _ in range(K): + spin_choices = [] + if n_up > 0: + spin_choices.append(0) + if n_dn > 0: + spin_choices.append(1) + spin = spin_choices[rng.randint(0, len(spin_choices))] + if spin == 0: + idx = rng.randint(0, n_up) + r_up = np.asarray(r_up).copy() + r_up[idx] = r_up[idx] + 0.1 * rng.randn(3) + moved_spin_is_up = True + moved_index = idx + else: + idx = rng.randint(0, n_dn) + r_dn = np.asarray(r_dn).copy() + r_dn[idx] = r_dn[idx] + 0.1 * rng.randn(3) + moved_spin_is_up = False + moved_index = idx + + state = _advance_grads_laplacian_Jastrow_two_body_streaming_state( + jastrow_two_body_data, + state, + jax.numpy.asarray(moved_spin_is_up), + jax.numpy.asarray(moved_index, dtype=jax.numpy.int32), + jax.numpy.asarray(r_up), + jax.numpy.asarray(r_dn), + ) + + # Cached r_up/r_dn inside the streaming state must track the moves. + np.testing.assert_allclose(np.asarray(state.r_up_carts), np.asarray(r_up), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.r_dn_carts), np.asarray(r_dn), atol=atol, rtol=rtol) + + g_up_full, g_dn_full, l_up_full, l_dn_full = compute_grads_and_laplacian_Jastrow_two_body( + jastrow_two_body_data, jax.numpy.asarray(r_up), jax.numpy.asarray(r_dn) + ) + np.testing.assert_allclose(np.asarray(state.grad_J2_up), np.asarray(g_up_full), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.grad_J2_dn), np.asarray(g_dn_full), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.lap_J2_up), np.asarray(l_up_full), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.lap_J2_dn), np.asarray(l_dn_full), atol=atol, rtol=rtol) + + +@pytest.mark.parametrize( + "trexio_file", + ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"], +) +def test_streaming_J3_state_against_full(trexio_file): + """K random single-electron moves advanced via the J3 streaming state must + match a fresh init at the resulting configuration (and the existing analytic + full computation) within strict tolerance.""" + import os + + from jqmc.jastrow_factor import ( + _advance_grads_laplacian_Jastrow_three_body_streaming_state, + _init_grads_laplacian_Jastrow_three_body_streaming_state, + ) + from jqmc.trexio_wrapper import read_trexio_file + + (_, aos_data, _, _, geminal_mo_data, _) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + + rng = np.random.RandomState(0) + jastrow_threebody_data = Jastrow_three_body_data.init_jastrow_three_body_data( + orb_data=aos_data, random_init=True, random_scale=1.0e-3 + ) + n_up = geminal_mo_data.num_electron_up + n_dn = geminal_mo_data.num_electron_dn + + r_up = 4.0 * rng.rand(n_up, 3) - 2.0 + r_dn = 4.0 * rng.rand(n_dn, 3) - 2.0 + + state = _init_grads_laplacian_Jastrow_three_body_streaming_state(jastrow_threebody_data, r_up, r_dn) + + K = 32 + atol, rtol = get_tolerance("wf_kinetic", "strict") + for _ in range(K): + # pick a random single-electron move (alternating spins when available) + spin_choices = [] + if n_up > 0: + spin_choices.append(0) + if n_dn > 0: + spin_choices.append(1) + spin = spin_choices[rng.randint(0, len(spin_choices))] + if spin == 0: + idx = rng.randint(0, n_up) + r_up = np.asarray(r_up).copy() + r_up[idx] = r_up[idx] + 0.1 * rng.randn(3) + moved_spin_is_up = True + moved_index = idx + else: + idx = rng.randint(0, n_dn) + r_dn = np.asarray(r_dn).copy() + r_dn[idx] = r_dn[idx] + 0.1 * rng.randn(3) + moved_spin_is_up = False + moved_index = idx + + state = _advance_grads_laplacian_Jastrow_three_body_streaming_state( + jastrow_threebody_data, + state, + jax.numpy.asarray(moved_spin_is_up), + jax.numpy.asarray(moved_index, dtype=jax.numpy.int32), + jax.numpy.asarray(r_up), + jax.numpy.asarray(r_dn), + ) + + fresh = _init_grads_laplacian_Jastrow_three_body_streaming_state(jastrow_threebody_data, r_up, r_dn) + np.testing.assert_allclose(np.asarray(state.grad_J3_up), np.asarray(fresh.grad_J3_up), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.grad_J3_dn), np.asarray(fresh.grad_J3_dn), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.lap_J3_up), np.asarray(fresh.lap_J3_up), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.lap_J3_dn), np.asarray(fresh.lap_J3_dn), atol=atol, rtol=rtol) + + # cross-check against the existing analytic full computation + g3u_full, g3d_full, l3u_full, l3d_full = compute_grads_and_laplacian_Jastrow_three_body( + jastrow_threebody_data, jax.numpy.asarray(r_up), jax.numpy.asarray(r_dn) + ) + np.testing.assert_allclose(np.asarray(state.grad_J3_up), np.asarray(g3u_full), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.grad_J3_dn), np.asarray(g3d_full), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.lap_J3_up), np.asarray(l3u_full), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(state.lap_J3_dn), np.asarray(l3d_full), atol=atol, rtol=rtol) + + if __name__ == "__main__": from logging import Formatter, StreamHandler, getLogger diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index 64cb2b6c..cb4b6fbc 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -56,6 +56,7 @@ from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 from jqmc.wavefunction import ( # noqa: E402 Wavefunction_data, + _advance_kinetic_energy_all_elements_streaming_state, _compute_discretized_kinetic_energy_debug, _compute_kinetic_energy_all_elements_auto, _compute_kinetic_energy_all_elements_debug, @@ -63,6 +64,8 @@ _compute_kinetic_energy_auto, _compute_kinetic_energy_debug, _compute_nodal_distance_debug, + _init_kinetic_energy_all_elements_streaming_state, + _kinetic_energy_from_streaming_state, compute_discretized_kinetic_energy, compute_discretized_kinetic_energy_fast_update, compute_kinetic_energy, @@ -655,6 +658,380 @@ def test_evaluate_ln_wavefunction_fast_backward(trexio_file): ) +# --------------------------------------------------------------------------- +# Streaming kinetic-energy state tests (PR1: J3 streaming) +# --------------------------------------------------------------------------- + + +def _build_A_inv_from_carts(geminal_data, r_up_jnp, r_dn_jnp): + """Compute A_inv = G(r_up, r_dn)^{-1} via SVD (matches the fast-update warning).""" + A = compute_geminal_all_elements( + geminal_data=geminal_data, + r_up_carts=r_up_jnp, + r_dn_carts=r_dn_jnp, + ) + return jnp.linalg.inv(A) + + +def _streaming_step_consistency_one(wavefunction_data, r_up0, r_dn0, K, atol, rtol, seed=0): + """Run K random single-electron moves through the streaming state and + compare the resulting kinetic energies with a fresh fast-update call at + the final configuration.""" + rng = np.random.RandomState(seed) + r_up = np.asarray(r_up0, dtype=np.float64).copy() + r_dn = np.asarray(r_dn0, dtype=np.float64).copy() + n_up = r_up.shape[0] + n_dn = r_dn.shape[0] + + A_inv = _build_A_inv_from_carts(wavefunction_data.geminal_data, jnp.asarray(r_up), jnp.asarray(r_dn)) + state = _init_kinetic_energy_all_elements_streaming_state( + wavefunction_data=wavefunction_data, + r_up_carts=jnp.asarray(r_up), + r_dn_carts=jnp.asarray(r_dn), + geminal_inverse=A_inv, + ) + + for _ in range(K): + choices = [] + if n_up > 0: + choices.append(0) + if n_dn > 0: + choices.append(1) + spin = choices[rng.randint(0, len(choices))] + if spin == 0: + idx = rng.randint(0, n_up) + r_up = r_up.copy() + r_up[idx] = r_up[idx] + 0.05 * rng.randn(3) + moved_spin_is_up = True + moved_index = idx + else: + idx = rng.randint(0, n_dn) + r_dn = r_dn.copy() + r_dn[idx] = r_dn[idx] + 0.05 * rng.randn(3) + moved_spin_is_up = False + moved_index = idx + + # rebuild A_inv at the new configuration (mirrors what Sherman-Morrison + # produces in the GFMC loop, modulo round-off — comparing at the same + # numerical reference here). + A_inv = _build_A_inv_from_carts(wavefunction_data.geminal_data, jnp.asarray(r_up), jnp.asarray(r_dn)) + state = _advance_kinetic_energy_all_elements_streaming_state( + wavefunction_data=wavefunction_data, + state=state, + moved_spin_is_up=jnp.asarray(moved_spin_is_up), + moved_index=jnp.asarray(moved_index, dtype=jnp.int32), + r_up_carts_new=jnp.asarray(r_up), + r_dn_carts_new=jnp.asarray(r_dn), + A_new_inv=A_inv, + ) + + ke_up_stream, ke_dn_stream = _kinetic_energy_from_streaming_state(state) + ke_up_fresh, ke_dn_fresh = compute_kinetic_energy_all_elements_fast_update( + wavefunction_data=wavefunction_data, + r_up_carts=jnp.asarray(r_up), + r_dn_carts=jnp.asarray(r_dn), + geminal_inverse=A_inv, + ) + np.testing.assert_allclose(np.asarray(ke_up_stream), np.asarray(ke_up_fresh), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(ke_dn_stream), np.asarray(ke_dn_fresh), atol=atol, rtol=rtol) + + +def _build_wavefunction_J3(trexio_file, j2_type="exp", with_J1=False, with_J2=True): + """Build a Wavefunction_data with J3 + optional J1/J2 from a trexio file. + + PR1 streaming requires J3 to be present (the dispatch demands it). + """ + from jqmc.jastrow_factor import Jastrow_one_body_data + + ( + structure_data, + aos_data, + _, + _, + geminal_mo_data, + _, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + + if with_J1: + jastrow_one_body_data = Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=0.5, + structure_data=structure_data, + core_electrons=tuple([0] * len(structure_data.atomic_numbers)), + jastrow_1b_type="pade", + ) + else: + jastrow_one_body_data = None + + if with_J2: + jastrow_two_body_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=1.0, jastrow_2b_type=j2_type) + else: + jastrow_two_body_data = None + + jastrow_three_body_data = Jastrow_three_body_data.init_jastrow_three_body_data( + orb_data=aos_data, random_init=True, random_scale=1.0e-3 + ) + + jastrow_data = Jastrow_data( + jastrow_one_body_data=jastrow_one_body_data, + jastrow_two_body_data=jastrow_two_body_data, + jastrow_three_body_data=jastrow_three_body_data, + ) + wavefunction_data = Wavefunction_data(geminal_data=geminal_mo_data, jastrow_data=jastrow_data) + return wavefunction_data, geminal_mo_data + + +@pytest.mark.parametrize( + "trexio_file", + ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"], +) +def test_streaming_kinetic_energy_step_consistency(trexio_file): + """K=32 random single-electron moves advanced via the streaming kinetic + state must reproduce the fresh fast-update kinetic energy at the resulting + configuration within strict tolerance.""" + wf, gem = _build_wavefunction_J3(trexio_file) + n_up = gem.num_electron_up + n_dn = gem.num_electron_dn + rng = np.random.RandomState(0) + r_up0 = 4.0 * rng.rand(n_up, 3) - 2.0 + r_dn0 = 4.0 * rng.rand(n_dn, 3) - 2.0 + atol, rtol = get_tolerance("wf_kinetic", "strict") + _streaming_step_consistency_one(wf, r_up0, r_dn0, K=32, atol=atol, rtol=rtol) + + +@pytest.mark.parametrize("K", [32, 100, 1000]) +def test_streaming_kinetic_drift_accumulation(K): + """Drift accumulation: K-step advance vs fresh init at config_K must stay + within ``loose`` tolerance even at K=1000, which sets the safety margin + for ``num_mcmc_per_measurement``.""" + wf, gem = _build_wavefunction_J3("H2_ae_ccpvdz_cart.h5") + rng = np.random.RandomState(1) + r_up0 = 4.0 * rng.rand(gem.num_electron_up, 3) - 2.0 + r_dn0 = 4.0 * rng.rand(gem.num_electron_dn, 3) - 2.0 + atol, rtol = get_tolerance("wf_kinetic", "loose") + _streaming_step_consistency_one(wf, r_up0, r_dn0, K=K, atol=atol, rtol=rtol, seed=2) + + +@pytest.mark.parametrize( + "trexio_file", + ["H2_ae_ccpvdz_cart.h5", "Li_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"], +) +def test_streaming_kinetic_edge_cases(trexio_file): + """Edge cases: small electron counts and ``N_up != N_dn`` (Li, N) must + still match the fresh fast-update result.""" + wf, gem = _build_wavefunction_J3(trexio_file) + rng = np.random.RandomState(3) + r_up0 = 4.0 * rng.rand(gem.num_electron_up, 3) - 2.0 + r_dn0 = 4.0 * rng.rand(gem.num_electron_dn, 3) - 2.0 + atol, rtol = get_tolerance("wf_kinetic", "strict") + _streaming_step_consistency_one(wf, r_up0, r_dn0, K=24, atol=atol, rtol=rtol, seed=4) + + +@pytest.mark.parametrize("jastrow_combo", ["J3_only", "J1_J3", "J2_J3", "J1_J2_J3"]) +def test_streaming_kinetic_jastrow_combinations(jastrow_combo): + """Streaming path must work for every J3-containing Jastrow combination + (PR1 dispatch requires J3 + ``jastrow_nn_data is None``).""" + with_J1 = "J1" in jastrow_combo + with_J2 = "J2" in jastrow_combo + wf, gem = _build_wavefunction_J3("water_ccecp_ccpvqz.h5", with_J1=with_J1, with_J2=with_J2) + rng = np.random.RandomState(5) + r_up0 = 4.0 * rng.rand(gem.num_electron_up, 3) - 2.0 + r_dn0 = 4.0 * rng.rand(gem.num_electron_dn, 3) - 2.0 + atol, rtol = get_tolerance("wf_kinetic", "strict") + _streaming_step_consistency_one(wf, r_up0, r_dn0, K=24, atol=atol, rtol=rtol, seed=6) + + +def test_streaming_kinetic_walker_axis_vmap(): + """``vmap`` over the walker axis must produce results equal to the + independent per-walker streaming chains. Confirms the state pytree carries + walkers correctly along the leading axis.""" + wf, gem = _build_wavefunction_J3("H2_ae_ccpvdz_cart.h5") + n_walkers = 4 + rng = np.random.RandomState(7) + r_up_w = jnp.asarray(4.0 * rng.rand(n_walkers, gem.num_electron_up, 3) - 2.0) + r_dn_w = jnp.asarray(4.0 * rng.rand(n_walkers, gem.num_electron_dn, 3) - 2.0) + + # Per-walker A_inv and initial state, computed via vmap. + def _make_init_state(r_up, r_dn): + A_inv = _build_A_inv_from_carts(wf.geminal_data, r_up, r_dn) + return _init_kinetic_energy_all_elements_streaming_state( + wavefunction_data=wf, + r_up_carts=r_up, + r_dn_carts=r_dn, + geminal_inverse=A_inv, + ), A_inv + + states, A_invs = jax.vmap(_make_init_state, in_axes=(0, 0))(r_up_w, r_dn_w) + + # Single up-electron move on walker 0 only; other walkers see the same + # advance call but with their own (unchanged) inputs. + moved_spin_is_up = jnp.asarray([True] * n_walkers) + moved_index = jnp.asarray([0] * n_walkers, dtype=jnp.int32) + + # Apply the same delta to electron 0 across walkers (just to exercise the + # vmap; the per-walker chains remain independent because `state` and + # `r_*_carts_new` are walker-batched). + delta = 0.05 * rng.randn(3) + r_up_w_new = r_up_w.at[:, 0, :].add(jnp.asarray(delta)) + A_invs_new = jax.vmap(lambda ru, rd: _build_A_inv_from_carts(wf.geminal_data, ru, rd), in_axes=(0, 0))(r_up_w_new, r_dn_w) + + advance_vmapped = jax.vmap( + lambda st, msi, mi, ru, rd, ai: _advance_kinetic_energy_all_elements_streaming_state( + wavefunction_data=wf, + state=st, + moved_spin_is_up=msi, + moved_index=mi, + r_up_carts_new=ru, + r_dn_carts_new=rd, + A_new_inv=ai, + ), + in_axes=(0, 0, 0, 0, 0, 0), + ) + states_new = advance_vmapped(states, moved_spin_is_up, moved_index, r_up_w_new, r_dn_w, A_invs_new) + + # Reference: fresh evaluation per walker. + ke_up_v, ke_dn_v = jax.vmap(_kinetic_energy_from_streaming_state)(states_new) + atol, rtol = get_tolerance("wf_kinetic", "strict") + for w in range(n_walkers): + ke_up_ref, ke_dn_ref = compute_kinetic_energy_all_elements_fast_update( + wavefunction_data=wf, + r_up_carts=r_up_w_new[w], + r_dn_carts=r_dn_w[w], + geminal_inverse=A_invs_new[w], + ) + np.testing.assert_allclose(np.asarray(ke_up_v[w]), np.asarray(ke_up_ref), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(ke_dn_v[w]), np.asarray(ke_dn_ref), atol=atol, rtol=rtol) + + +# --------------------------------------------------------------------------- +# j3_state-forwarding consistency tests (PR4/PR5: ECP non-local AO reuse + +# discretized kinetic AO reuse) +# +# These verify the Python-static dispatch in +# ``_compute_ratio_Jastrow_part_rank1_update`` and +# ``_compute_ratio_Jastrow_part_split_spin``: the with-state path must produce +# identical Jastrow ratios (and therefore identical kinetic / ECP elements) as +# the no-state path when the streaming state is consistent with +# ``(r_up_carts, r_dn_carts)``. +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "trexio_file,jastrow_combo", + [ + ("water_ccecp_ccpvqz.h5", "J3_only"), + ("water_ccecp_ccpvqz.h5", "J1_J2_J3"), + ("H2_ae_ccpvdz_cart.h5", "J2_J3"), + ("N_ae_ccpvdz_cart.h5", "J1_J3"), + ], +) +def test_streaming_discretized_kinetic_j3_state_consistency(trexio_file, jastrow_combo): + """``compute_discretized_kinetic_energy_fast_update`` must return the same + kinetic mesh elements whether the J3 streaming state is forwarded or not. + + Validates the Python-static dispatch in + ``_compute_ratio_Jastrow_part_rank1_update`` (the ratio kernel called by + the discretized kinetic for the LRDMC mesh). + """ + with_J1 = "J1" in jastrow_combo + with_J2 = "J2" in jastrow_combo + wf, gem = _build_wavefunction_J3(trexio_file, with_J1=with_J1, with_J2=with_J2) + rng = np.random.RandomState(11) + r_up = jnp.asarray(4.0 * rng.rand(gem.num_electron_up, 3) - 2.0) + r_dn = jnp.asarray(4.0 * rng.rand(gem.num_electron_dn, 3) - 2.0) + A_inv = _build_A_inv_from_carts(wf.geminal_data, r_up, r_dn) + state = _init_kinetic_energy_all_elements_streaming_state( + wavefunction_data=wf, r_up_carts=r_up, r_dn_carts=r_dn, geminal_inverse=A_inv + ) + + alat = 0.40 + RT = jnp.eye(3, dtype=jnp.float64) + + rup_ref, rdn_ref, ke_ref = compute_discretized_kinetic_energy_fast_update( + alat=alat, + wavefunction_data=wf, + A_old_inv=A_inv, + r_up_carts=r_up, + r_dn_carts=r_dn, + RT=RT, + j3_state=None, + ) + rup_st, rdn_st, ke_st = compute_discretized_kinetic_energy_fast_update( + alat=alat, + wavefunction_data=wf, + A_old_inv=A_inv, + r_up_carts=r_up, + r_dn_carts=r_dn, + RT=RT, + j3_state=state.j3_state, + ) + + atol, rtol = get_tolerance("wf_kinetic", "strict") + np.testing.assert_allclose(np.asarray(rup_st), np.asarray(rup_ref), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(rdn_st), np.asarray(rdn_ref), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(ke_st), np.asarray(ke_ref), atol=atol, rtol=rtol) + + +@pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5"]) +@pytest.mark.parametrize("jastrow_combo", ["J3_only", "J2_J3", "J1_J2_J3"]) +def test_streaming_ecp_nonlocal_j3_state_consistency(trexio_file, jastrow_combo): + """``compute_ecp_non_local_parts_nearest_neighbors_fast_update`` (tmove + path, ``flag_determinant_only=False``) must return identical V_nonlocal + whether the J3 streaming state is forwarded or not. + + Validates the Python-static dispatch in + ``_compute_ratio_Jastrow_part_split_spin`` (the ratio kernel used for the + block-structured non-local ECP grid). + """ + from jqmc.coulomb_potential import compute_ecp_non_local_parts_nearest_neighbors_fast_update + + with_J1 = "J1" in jastrow_combo + with_J2 = "J2" in jastrow_combo + (_, _, _, _, _, coulomb_potential_data) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + wf, gem = _build_wavefunction_J3(trexio_file, with_J1=with_J1, with_J2=with_J2) + rng = np.random.RandomState(13) + r_up = jnp.asarray(4.0 * rng.rand(gem.num_electron_up, 3) - 2.0) + r_dn = jnp.asarray(4.0 * rng.rand(gem.num_electron_dn, 3) - 2.0) + A_inv = _build_A_inv_from_carts(wf.geminal_data, r_up, r_dn) + state = _init_kinetic_energy_all_elements_streaming_state( + wavefunction_data=wf, r_up_carts=r_up, r_dn_carts=r_dn, geminal_inverse=A_inv + ) + + RT = jnp.eye(3, dtype=jnp.float64) + + rup_ref, rdn_ref, V_ref, sV_ref = compute_ecp_non_local_parts_nearest_neighbors_fast_update( + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wf, + r_up_carts=r_up, + r_dn_carts=r_dn, + RT=RT, + A_old_inv=A_inv, + flag_determinant_only=False, + j3_state=None, + ) + rup_st, rdn_st, V_st, sV_st = compute_ecp_non_local_parts_nearest_neighbors_fast_update( + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wf, + r_up_carts=r_up, + r_dn_carts=r_dn, + RT=RT, + A_old_inv=A_inv, + flag_determinant_only=False, + j3_state=state.j3_state, + ) + + atol, rtol = get_tolerance("wf_kinetic", "strict") + np.testing.assert_allclose(np.asarray(rup_st), np.asarray(rup_ref), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(rdn_st), np.asarray(rdn_ref), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(V_st), np.asarray(V_ref), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(sV_st), np.asarray(sV_ref), atol=atol, rtol=rtol) + + if __name__ == "__main__": from logging import Formatter, StreamHandler, getLogger From 0f283dc870db3fde32da00c61f2662f6bbf10727 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:40:20 +0900 Subject: [PATCH 20/97] Fuse AO/MO value/grad/lap into a single dispatch on hot paths - Add `compute_AOs_value_grad_lap` and `compute_MOs_value_grad_lap`, which share the heavy block (`exp` / polynomial chain / `S_l_m`) across value, gradient, and Laplacian evaluation instead of recomputing it three times. - Migrate the streaming-advance hot path (Det/J3 single-electron updates), the fast/init paths, and the J3 forward grad/lap path to the fused API via `Geminal_data.compute_orb_value_grad_lap_api` and `_three_body_orb_apis`. --- jqmc/_precision.py | 52 +++--- jqmc/atomic_orbital.py | 297 ++++++++++++++++++++++++++++++---- jqmc/determinant.py | 133 ++++++++------- jqmc/jastrow_factor.py | 127 ++++++++++----- jqmc/molecular_orbital.py | 57 ++++++- jqmc/wavefunction.py | 11 ++ tests/test_AOs.py | 126 ++++++++++++++- tests/test_MOs.py | 113 +++++++++++-- tests/test_determinant.py | 26 +-- tests/test_jastrow.py | 36 ++++- tests/test_mixed_precision.py | 12 +- 11 files changed, 788 insertions(+), 202 deletions(-) diff --git a/jqmc/_precision.py b/jqmc/_precision.py index eff8d465..7cd3a113 100644 --- a/jqmc/_precision.py +++ b/jqmc/_precision.py @@ -20,7 +20,7 @@ Principle 2 — A module may own multiple Precision Zones. ------------------------------------------------------------ Different code paths in the same module legitimately need different precisions -(e.g. ``ao_eval`` vs ``ao_grad``, or ``det_eval`` vs ``det_ratio``). Each +(e.g. ``ao_eval`` vs ``ao_grad_lap``, or ``det_eval`` vs ``det_ratio``). Each zone is named for its *purpose*, not for its dtype. ------------------------------------------------------------ @@ -174,8 +174,7 @@ def compute_coulomb(r_carts, R_carts): Zone Owning module Default Mixed risk E_L path ================== ================================= ========= ======== ===== ========= ``ao_eval`` atomic_orbital.py (forward) float64 float32 low core -``ao_grad`` atomic_orbital.py (gradient) float64 float32 low core -``ao_lap`` atomic_orbital.py (Laplacian) float64 float64 high§ core +``ao_grad_lap`` atomic_orbital.py (grad/Lap) float64 float64 high§ core ``mo_eval`` molecular_orbital.py (forward) float64 float64 high* core ``mo_grad`` molecular_orbital.py (gradient) float64 float64 high core ``mo_lap`` molecular_orbital.py (Laplacian) float64 float64 high core @@ -203,15 +202,22 @@ def compute_coulomb(r_carts, R_carts): (E_L depends on *derivatives* of ln|Psi|). Diagnostics show zero E_L bias when these zones alone are fp32. -§ ``ao_lap`` is fp64 even in mixed mode because the analytic Laplacian -kernel for spherical AOs contains catastrophic cancellation +§ ``ao_grad_lap`` is fp64 even in mixed mode because the analytic +Laplacian kernel for spherical AOs contains catastrophic cancellation (``4 Z² r² − 6 Z`` and ``(safe_div − 2 Z·base)² − safe_div² − 2 Z`` terms) that fp32 cannot resolve for tight Gaussians. Diagnostic ``bug/fp32/diag_07_ao_grad_vs_lap_split.py`` showed that ``ao_lap=fp32`` alone reproduces the full atomic-force bias (``max|dF| ≈ 1.9 Ha/bohr`` on N₂ at scale=0.3, ``≈ 2e−2 Ha/bohr`` on -the water-cluster-8 system), while ``ao_grad=fp32`` alone is safe -(``max|dF| < 8e−3 Ha/bohr``). +the water-cluster-8 system); the historical ``ao_grad=fp32`` zone was +safe in isolation (``max|dF| < 8e−3 Ha/bohr``) but is merged here with +``ao_lap`` because the fused ``compute_AOs_value_grad_lap`` kernel +shares one heavy expression (``exp / pow / phi / S_l_m``) across grad +and lap. Running that shared kernel at fp32 would break the lap path, +so the unified zone is fp64 always — a small extra cost on the +standalone ``compute_AOs_grad`` (which is not on the per-step hot +path) in exchange for a single source of truth for the shared kernel +dtype. ‡ ``det_ratio`` and ``jastrow_ratio`` affect E_L **indirectly** through the ECP non-local potential, which evaluates Psi(R')/Psi(R) on a @@ -237,7 +243,7 @@ def _compute_AOs_kernel(aos_data, r_carts): # NOTE: never reach for another module's zone (e.g. # ``get_dtype_jnp("local_energy")``) here — that violates # Principle 1 (zone ↔ owning module is 1:1). atomic_orbital.py - # may only consult ao_eval / ao_grad / ao_lap. + # may only consult ao_eval / ao_grad_lap. dtype_jnp = get_dtype_jnp("ao_eval") R_carts = aos_data._atomic_center_carts_jnp diff = (r_carts - R_carts).astype(dtype_jnp) @@ -288,8 +294,7 @@ def _compute_AOs_kernel(aos_data, r_carts): _FULL_PRECISION: dict[str, str] = { # atomic_orbital.py "ao_eval": "float64", # AO forward evaluation - "ao_grad": "float64", # AO gradient - "ao_lap": "float64", # AO Laplacian + "ao_grad_lap": "float64", # AO gradient + Laplacian (unified for fused kernel) # molecular_orbital.py "mo_eval": "float64", # MO forward evaluation (mo_coef @ AO) "mo_grad": "float64", # MO gradient @@ -315,17 +320,12 @@ def _compute_AOs_kernel(aos_data, r_carts): } # --- mode="mixed" (recommended mixed precision) --- -# Five "low risk" zones drop to float32: +# Four "low risk" zones drop to float32: # # ao_eval - smooth Gaussian basis kernel; the dominant cost. # The downstream consumer (mo_eval / det_eval / # jastrow_eval) is fp64 and explicitly casts the AO # result up before any sensitive arithmetic. -# ao_grad - AO analytic gradient kernel; same O(N_ao × N_e) -# cost as ao_eval. Diagnostics -# (bug/fp32/diag_07) show grad-only fp32 yields -# max|dF| < 8e-3 Ha/bohr (relative bias ~5e-5 on -# water-cluster-8) — well within chemical accuracy. # jastrow_eval - smooth correlation function value (pre-exp). # jastrow_grad_lap - nabla J, nabla^2 J; smooth Jastrow factor, low # cancellation. Diagnostics show bias < 8e-06 Ha @@ -342,12 +342,19 @@ def _compute_AOs_kernel(aos_data, r_carts): # unacceptable bias on E_L for ~32-electron systems, OR the # kernel is cheap enough that fp32 is not worth the bias: # -# ao_lap - analytic Laplacian kernel for spherical/Cartesian AOs -# contains catastrophic cancellation (``4 Z² r² − 6 Z`` -# and ``(safe_div − 2 Z·base)² − safe_div² − 2 Z``). +# ao_grad_lap - analytic gradient + Laplacian kernel for spherical/ +# Cartesian AOs. Lap arithmetic contains catastrophic +# cancellation (``4 Z² r² − 6 Z`` and +# ``(safe_div − 2 Z·base)² − safe_div² − 2 Z``). # diag_07 showed lap=fp32 alone yields max|dF| ≈ 1.9 # Ha/bohr on N₂ (scale=0.3), reproducing the entire -# bias of grad+lap=fp32. fp64 mandatory. +# bias of grad+lap=fp32. fp64 mandatory. This zone +# merges the historical ``ao_grad`` (which was safe at +# fp32 in isolation) with ``ao_lap`` because the fused +# ``compute_AOs_value_grad_lap`` kernel evaluates +# ``exp / pow / phi / S_l_m`` once and reuses it across +# grad and lap; running the shared path at fp32 would +# break the lap output. # coulomb - sum of 1/r + ECP spherical quadrature. Cheap # (O(N_e^2) el-el + O(N_e * N_nuc) el-ion, vs # O(N_e * N_ao) AO eval) but contributes the @@ -376,8 +383,9 @@ def _compute_AOs_kernel(aos_data, r_carts): _MIXED_PRECISION: dict[str, str] = { # atomic_orbital.py "ao_eval": "float32", # low risk (heavy kernel) - "ao_grad": "float32", # low risk (smooth grad kernel; bias < 8e-3 Ha/bohr atomic force) - "ao_lap": "float64", # high risk (catastrophic cancellation in 4Z²r²-6Z terms) + "ao_grad_lap": "float64", # high risk (catastrophic cancellation in 4Z²r²-6Z terms; + # unified zone — historical ao_grad was safe at fp32 but is merged with ao_lap so + # the fused compute_AOs_value_grad_lap kernel can share one heavy kernel at fp64) # molecular_orbital.py "mo_eval": "float64", # high risk (feeds det_eval) "mo_grad": "float64", # high risk diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index ed34dc05..af03e7de 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -1,10 +1,22 @@ """Atomic Orbitals module. -Module containing classes and methods related to Atomic Orbitals +Module containing classes and methods related to Atomic Orbitals. Precision Zones: - - ``orb_eval``: forward AO evaluation (compute_AOs and internal helpers). - - ``kinetic``: AO gradient and Laplacian (compute_AOs_grad, compute_AOs_laplacian). + - ``ao_eval``: forward AO evaluation (compute_AOs and internal helpers). + - ``ao_grad_lap``: AO gradient and Laplacian (compute_AOs_grad, + compute_AOs_laplacian). Pinned to fp64 even in mixed mode — the + shared kernel must avoid catastrophic cancellation in the + Laplacian arithmetic (e.g. ``4 Z^2 r^2 - 6 Z`` for s-type AOs). + +The fused :func:`compute_AOs_value_grad_lap` API returns ``(val, gx, gy, +gz, lap)`` from a single dispatch — the heavy block (``exp``, polynomial +chain, ``S_l_m``) is shared across val/grad/lap instead of recomputed +three times. ``val`` is downcast to ``ao_eval`` while grad/lap stay in +``ao_grad_lap``. Use it when value, gradient, and Laplacian are all +needed at the same call site (kinetic-energy estimators, GFMC streaming +advance); otherwise prefer the standalone APIs (:func:`compute_AOs`, +:func:`compute_AOs_grad`, :func:`compute_AOs_laplacian`). See :mod:`jqmc._precision` for details. """ @@ -1435,7 +1447,11 @@ def _aos_sphe_to_cart(aos_data: AOs_sphe_data | AOs_cart_data) -> tuple[AOs_cart tuple: (AOs_cart_data, transform_matrix) where transform_matrix maps spherical -> Cartesian coefficients with shape (num_ao_sph, num_ao_cart). """ - dtype_np = get_dtype_np("ao_eval") + # I/O / setup-time basis conversion: hardcode fp64 (no precision-zone + # involvement). The transform matrix carries pure mathematical conversion + # constants and feeds the MO coefficient transform; downcasting it to a + # mixed-mode fp32 zone leaks fp32 noise into fp64 mo_coefficients. + dtype_np = np.float64 if isinstance(aos_data, AOs_cart_data): transform_matrix = np.eye(aos_data.num_ao, dtype=dtype_np) return aos_data, transform_matrix @@ -1533,7 +1549,9 @@ def _aos_cart_to_sphe(aos_data: AOs_cart_data | AOs_sphe_data) -> tuple[AOs_sphe tuple: (AOs_sphe_data, transform_pinv) where transform_pinv maps Cartesian -> spherical coefficients with shape (num_ao_cart, num_ao_sph). """ - dtype_np = get_dtype_np("ao_eval") + # I/O / setup-time basis conversion: hardcode fp64 (no precision-zone + # involvement). See ``_aos_sphe_to_cart`` for the rationale. + dtype_np = np.float64 if isinstance(aos_data, AOs_sphe_data): transform_pinv = np.eye(aos_data.num_ao, dtype=dtype_np) return aos_data, transform_pinv @@ -2329,21 +2347,21 @@ def lnorm(l): def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Vectorized solid harmonics values, gradients, and Laplacians. - Pinned to the ``ao_lap`` zone (fp64 in mixed mode). The gradient - consumer (``_compute_AOs_grad_analytic_sphe``) lives in the - ``ao_grad`` zone (fp32 in mixed mode); it is responsible for - down-casting the grad output of this helper to its own zone at the - use site (Principle 3b). Running the helper at the higher of the - two precisions is intentional — the solid-harmonics polynomial - expansion is cheap (49 × num_R × num_e) compared to the contracted - AO formulas, so the perf cost of fp64 here is small while keeping - the laplacian path numerically safe. + Pinned to the ``ao_grad_lap`` zone (fp64 in both full and mixed mode). + Both grad and lap consumers (``_compute_AOs_grad_analytic_sphe`` and + ``_compute_AOs_laplacian_analytic_sphe``) live in the same + ``ao_grad_lap`` zone, so no further down-cast is required at the + consumer site. Running the helper at fp64 is mandated by the + catastrophic cancellation in the laplacian arithmetic + (``4 Z^2 r^2 - 6 Z``); the solid-harmonics polynomial expansion is + cheap (49 × num_R × num_e) compared to the contracted AO formulas, + so the cost of fp64 here is small. Returns: tuple: (values, grads, laps) where values has shape (49, num_R, num_r), grads has shape (49, num_R, num_r, 3), and laps has shape (49, num_R, num_r). """ - dtype_jnp = get_dtype_jnp("ao_lap") + dtype_jnp = get_dtype_jnp("ao_grad_lap") S_L_M_COEFFS = ( jnp.array([1.0], dtype=dtype_jnp), jnp.array([1.0], dtype=dtype_jnp), @@ -2590,9 +2608,9 @@ def _single_val_grad_lap(diff: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.A @jit def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for Cartesian AOs (contracted).""" - dtype_jnp = get_dtype_jnp("ao_lap") + dtype_jnp = get_dtype_jnp("ao_grad_lap") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) - # via JAX promotion, then downcast to the ao_lap zone (Principle 3b). + # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage # accessor on the basis-data dataclass. R_carts = aos_data._atomic_center_carts_prim_jnp @@ -2639,9 +2657,9 @@ def _second_component(base, n): @jit def _compute_AOs_laplacian_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> jax.Array: """Analytic Laplacian for spherical AOs (contracted).""" - dtype_jnp = get_dtype_jnp("ao_lap") + dtype_jnp = get_dtype_jnp("ao_grad_lap") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) - # via JAX promotion, then downcast to the ao_lap zone (Principle 3b). + # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage # accessor on the basis-data dataclass. R_carts = aos_data._atomic_center_carts_prim_jnp @@ -2902,9 +2920,9 @@ def _compute_AOs_laplacian_debug( @jit def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for Cartesian AOs (contracted).""" - dtype_jnp = get_dtype_jnp("ao_grad") + dtype_jnp = get_dtype_jnp("ao_grad_lap") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) - # via JAX promotion, then downcast to the ao_grad zone (Principle 3b). + # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage # accessor on the basis-data dataclass. R_carts = aos_data._atomic_center_carts_prim_jnp @@ -2954,9 +2972,9 @@ def _grad_component(base, n): @jit def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: """Analytic gradients for spherical AOs (contracted).""" - dtype_jnp = get_dtype_jnp("ao_grad") + dtype_jnp = get_dtype_jnp("ao_grad_lap") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) - # via JAX promotion, then downcast to the ao_grad zone (Principle 3b). + # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). # r_carts forwarded as-is (Principle 3a); R_carts read from fp64 storage # accessor on the basis-data dataclass. R_carts = aos_data._atomic_center_carts_prim_jnp @@ -2982,12 +3000,17 @@ def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarra r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) - max_ml, S_l_m_dup_all_l_m = _compute_S_l_m(r_R_diffs_uq) - # ``_compute_S_l_m_and_grad_lap`` is pinned to ``ao_lap`` (fp64); its - # grad output is therefore returned in fp64. Cast it down to the - # ``ao_grad`` zone at the use site (Principle 3b) so the contracted - # grad arithmetic below stays in this function's own zone. - _, S_l_m_grad_all_l_m, _ = _compute_S_l_m_and_grad_lap(r_R_diffs_uq) + # Use a single ``_compute_S_l_m_and_grad_lap`` call and reuse its value + # output instead of re-running ``_compute_S_l_m``. The helper is pinned + # to the ``ao_grad_lap`` zone (fp64), and this caller is also in the + # ``ao_grad_lap`` zone after PR1-A — so the ``.astype(dtype_jnp)`` calls + # below are no-ops at runtime but kept for explicit Principle 3b + # documentation. Lap output is unused — JAX DCE eliminates it because + # this whole function is inlined inside the caller's @jit and the lap + # branch never reaches a sink. + max_ml = 49 + S_l_m_dup_all_l_m, S_l_m_grad_all_l_m, _ = _compute_S_l_m_and_grad_lap(r_R_diffs_uq) + S_l_m_dup_all_l_m = S_l_m_dup_all_l_m.astype(dtype_jnp) S_l_m_grad_all_l_m = S_l_m_grad_all_l_m.astype(dtype_jnp) S_l_m_dup_all_l_m_reshaped = S_l_m_dup_all_l_m.reshape( @@ -3054,6 +3077,222 @@ def compute_AOs_grad(aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax.Array raise NotImplementedError("Analytic gradients implemented for Cartesian and spherical AOs only.") +@jit +def _compute_AOs_value_grad_lap_cart( + aos_data: AOs_cart_data, r_carts: jnp.ndarray +) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array, jax.Array]: + """Fused value/grad/lap for Cartesian AOs (contracted). + + Shared heavy block (``exp(-Z r^2)``, polynomial powers, ``phi``) is + evaluated once in the ``ao_grad_lap`` zone (fp64); only the value + output is downcast to ``ao_eval`` at the segment-sum site. See module + docstring for the full rationale. + """ + dtype_eval = get_dtype_jnp("ao_eval") + dtype_jnp = get_dtype_jnp("ao_grad_lap") + # Reconstruct r-R in caller-supplied precision (fp64) via JAX promotion + # then downcast to the shared ao_grad_lap zone (Principle 3b). Mirrors + # _compute_AOs_grad_analytic_cart / _compute_AOs_laplacian_analytic_cart + # so grad/lap parity vs the standalone APIs is bitwise in full mode. + R_carts = aos_data._atomic_center_carts_prim_jnp + diff = (r_carts[None, :, :] - R_carts[:, None, :]).astype(dtype_jnp) + c = aos_data._coefficients_jnp.astype(dtype_jnp) + Z = aos_data._exponents_jnp.astype(dtype_jnp) + l = aos_data._angular_momentums_prim_jnp + nx = aos_data._polynominal_order_x_prim_jnp + ny = aos_data._polynominal_order_y_prim_jnp + nz = aos_data._polynominal_order_z_prim_jnp + + N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) + N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z) ** l + N = jnp.sqrt(N_Z * N_fact) + + x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] + eps = get_eps("stabilizing_ao", dtype_jnp) + x = x + eps + y = y + eps + z = z + eps + r2 = jnp.sum(diff**2, axis=-1) + pref = c[:, None] * jnp.exp(-Z[:, None] * r2) + + def _pow(base, exp): + return jnp.where(exp[:, None] == 0, 1.0, base ** exp[:, None]) + + px, py, pz = _pow(x, nx), _pow(y, ny), _pow(z, nz) + # Shared body identical to the standalone grad/lap kernels (left-to-right + # multiplication). Strict (rtol=atol=0) parity vs compute_AOs_grad and + # compute_AOs_laplacian holds because the expression is bit-for-bit the + # same; parity vs compute_AOs is preserved up to a few ULPs because the + # standalone eval kernel uses a different multiplication ordering. + phi = N[:, None] * pref * px * py * pz # shared val/grad/lap body + + orbital_indices = aos_data._orbital_indices_jnp + num_segments = aos_data.num_ao + + # value finalize: only downcast site (Principle 3b). + val = jax.ops.segment_sum(phi.astype(dtype_eval), orbital_indices, num_segments=num_segments) + + # grad finalize (kept in ao_grad_lap zone — no cast). + def _grad_component(base, n): + safe_div = jnp.where(base != 0.0, n[:, None] / base, 0.0) + return phi * (safe_div - 2.0 * Z[:, None] * base) + + gx_dup = _grad_component(x, nx) + gy_dup = _grad_component(y, ny) + gz_dup = _grad_component(z, nz) + gx = jax.ops.segment_sum(gx_dup, orbital_indices, num_segments=num_segments) + gy = jax.ops.segment_sum(gy_dup, orbital_indices, num_segments=num_segments) + gz = jax.ops.segment_sum(gz_dup, orbital_indices, num_segments=num_segments) + + # lap finalize (kept in ao_grad_lap zone — no cast). + def _second_component(base, n): + safe_div = jnp.where(base != 0.0, n[:, None] / base, 0.0) + safe_div2 = jnp.where(base != 0.0, n[:, None] / (base**2), 0.0) + a = safe_div - 2.0 * Z[:, None] * base + return phi * (a**2 - safe_div2 - 2.0 * Z[:, None]) + + lap_dup = _second_component(x, nx) + _second_component(y, ny) + _second_component(z, nz) + lap = jax.ops.segment_sum(lap_dup, orbital_indices, num_segments=num_segments) + + return val, gx, gy, gz, lap + + +@jit +def _compute_AOs_value_grad_lap_sphe( + aos_data: AOs_sphe_data, r_carts: jnp.ndarray +) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array, jax.Array]: + """Fused value/grad/lap for spherical AOs (contracted). + + Shared heavy block (``exp(-Z r^2)``, ``_compute_S_l_m_and_grad_lap``, + ``pref = N_n * R_n * N_l_m``) is evaluated once in the ``ao_grad_lap`` + zone (fp64); only the value output is downcast to ``ao_eval`` at the + segment-sum site. The single ``_compute_S_l_m_and_grad_lap`` call + replaces the legacy 2-3x duplicate evaluations across the standalone + eval / grad / lap kernels. + """ + dtype_eval = get_dtype_jnp("ao_eval") + dtype_jnp = get_dtype_jnp("ao_grad_lap") + # Reconstruct r-R in caller-supplied precision (fp64) via JAX promotion + # then downcast to the shared ao_grad_lap zone (Principle 3b). Mirrors + # _compute_AOs_laplacian_analytic_sphe so grad/lap parity vs the + # standalone APIs is bitwise in full mode. + R_carts = aos_data._atomic_center_carts_prim_jnp + R_carts_unique = aos_data._atomic_center_carts_unique_jnp + r_R_diffs = (r_carts[None, :, :] - R_carts[:, None, :]).astype(dtype_jnp) + r_R_diffs_uq = (r_carts[None, :, :] - R_carts_unique[:, None, :]).astype(dtype_jnp) + nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp + c_jnp = aos_data._coefficients_jnp.astype(dtype_jnp) + Z_jnp = aos_data._exponents_jnp.astype(dtype_jnp) + l_jnp = aos_data._angular_momentums_prim_jnp + m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp + + l_f64 = l_jnp.astype(dtype_jnp) + Z_f64 = Z_jnp.astype(dtype_jnp) + factorial_l_plus_1 = jnp.exp(jscipy.special.gammaln(l_f64 + 2.0)) + factorial_2l_plus_2 = jnp.exp(jscipy.special.gammaln(2.0 * l_f64 + 3.0)) + + N_n_dup = jnp.sqrt( + (2.0 ** (2 * l_f64 + 3) * factorial_l_plus_1 * (2 * Z_f64) ** (l_f64 + 1.5)) / (factorial_2l_plus_2 * jnp.sqrt(jnp.pi)) + ) + N_l_m_dup = jnp.sqrt((2 * l_f64 + 1) / (4 * jnp.pi)) + + r_squared = jnp.sum(r_R_diffs**2, axis=-1) + R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) + + # Single S_l_m call returning (vals, grads, laps) — replaces the + # 2-3x duplicate evaluations across the legacy eval/grad/lap kernels. + S_l_m_vals_all, S_l_m_grads_all, S_l_m_laps_all = _compute_S_l_m_and_grad_lap(r_R_diffs_uq) + max_ml = S_l_m_vals_all.shape[0] + + S_l_m_vals_flat = S_l_m_vals_all.reshape((max_ml * S_l_m_vals_all.shape[1], S_l_m_vals_all.shape[2]), order="F") + S_l_m_grads_flat = S_l_m_grads_all.reshape((max_ml * S_l_m_grads_all.shape[1], S_l_m_grads_all.shape[2], 3), order="F") + S_l_m_laps_flat = S_l_m_laps_all.reshape((max_ml * S_l_m_laps_all.shape[1], S_l_m_laps_all.shape[2]), order="F") + + global_l_m_index = l_jnp**2 + (m_jnp + l_jnp) + global_R_l_m_index = nucleus_index_prim_jnp * max_ml + global_l_m_index + S_l_m_dup = S_l_m_vals_flat[global_R_l_m_index] + S_l_m_grad_dup = S_l_m_grads_flat[global_R_l_m_index] + S_l_m_lap_dup = S_l_m_laps_flat[global_R_l_m_index] + + # Shared body identical to the standalone grad/lap kernels. + pref = N_n_dup[:, None] * R_n_dup * N_l_m_dup[:, None] + AOs_dup = pref * S_l_m_dup + + orbital_indices = aos_data._orbital_indices_jnp + num_segments = aos_data.num_ao + + # value finalize: only downcast site (Principle 3b). + val = jax.ops.segment_sum(AOs_dup.astype(dtype_eval), orbital_indices, num_segments=num_segments) + + # grad finalize (kept in ao_grad_lap zone — no cast). + grad_from_R = AOs_dup[..., None] * (-2.0 * Z_jnp[:, None, None] * r_R_diffs) + grad_from_S = pref[..., None] * S_l_m_grad_dup + grad_dup = grad_from_R + grad_from_S + gx = jax.ops.segment_sum(grad_dup[..., 0], orbital_indices, num_segments=num_segments) + gy = jax.ops.segment_sum(grad_dup[..., 1], orbital_indices, num_segments=num_segments) + gz = jax.ops.segment_sum(grad_dup[..., 2], orbital_indices, num_segments=num_segments) + + # lap finalize (kept in ao_grad_lap zone — no cast). + grad_S_dot_r = jnp.sum(S_l_m_grad_dup * r_R_diffs, axis=-1) + lap_dup = ( + pref * S_l_m_lap_dup + + AOs_dup * (4.0 * (Z_jnp[:, None] ** 2) * r_squared - 6.0 * Z_jnp[:, None]) + - 4.0 * Z_jnp[:, None] * pref * grad_S_dot_r + ) + lap = jax.ops.segment_sum(lap_dup, orbital_indices, num_segments=num_segments) + + return val, gx, gy, gz, lap + + +def compute_AOs_value_grad_lap( + aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax.Array +) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array, jax.Array]: + """Fused evaluation of AO values, Cartesian gradients, and Laplacians. + + Returns ``(val, gx, gy, gz, lap)``. ``val`` is in the ``ao_eval`` zone + (fp32 in mixed mode, fp64 in full mode); ``gx``, ``gy``, ``gz``, and + ``lap`` are in the ``ao_grad_lap`` zone (fp64 in both modes). All + arrays have shape ``(num_ao, N_e)``. + + Use this when value, gradient, and Laplacian are all needed at the + same call site (kinetic energy, streaming-state initialisation / + advance). For value-only, grad-only, or lap-only call sites, prefer + the standalone APIs (``compute_AOs`` / ``compute_AOs_grad`` / + ``compute_AOs_laplacian``) — JAX DCE does not reliably eliminate the + unused outputs of this function across its ``@jit`` boundary. + + Mixed-precision design: shared body (``exp(-Z r^2)``, polynomial / + ``S_l_m``, ``phi`` / ``pref``) is computed once in ``ao_grad_lap`` + (fp64); ``val`` is downcast to ``ao_eval`` only at the segment-sum + site. ``gx`` / ``gy`` / ``gz`` / ``lap`` are kept in fp64 to protect + the laplacian's ``4 Z^2 r^2 - 6 Z`` cancellation. + + Args: + aos_data: ``AOs_cart_data`` or ``AOs_sphe_data`` describing primitive + parameters, angular info, contraction mapping, and centers (run + ``sanity_check()`` beforehand). + r_carts (jax.Array): Electron Cartesian coordinates, shape ``(N_e, 3)`` + (Bohr). Forwarded as-is (Principle 3a); the kernels reconstruct + ``r - R`` in fp64 internally to avoid catastrophic cancellation. + + Returns: + tuple: ``(val, gx, gy, gz, lap)``, each of shape ``(num_ao, N_e)``. + + Raises: + NotImplementedError: If ``aos_data`` is neither Cartesian nor spherical. + """ + # NOTE: do not pre-cast r_carts here. The kernels reconstruct r-R in + # fp64 internally to avoid catastrophic cancellation; a premature + # downcast in this wrapper would defeat that guard. + if isinstance(aos_data, AOs_cart_data): + return _compute_AOs_value_grad_lap_cart(aos_data, r_carts) + + if isinstance(aos_data, AOs_sphe_data): + return _compute_AOs_value_grad_lap_sphe(aos_data, r_carts) + + raise NotImplementedError("Fused AO value/grad/lap implemented for Cartesian and spherical AOs only.") + + def _compute_AOs_grad_autodiff( aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jnpt.ArrayLike ) -> tuple[jax.Array, jax.Array, jax.Array]: diff --git a/jqmc/determinant.py b/jqmc/determinant.py index a58d4ad9..f1fba6f4 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -67,9 +67,16 @@ compute_AOs, compute_AOs_grad, compute_AOs_laplacian, + compute_AOs_value_grad_lap, compute_overlap_matrix, ) -from .molecular_orbital import MOs_data, compute_MOs, compute_MOs_grad, compute_MOs_laplacian +from .molecular_orbital import ( + MOs_data, + compute_MOs, + compute_MOs_grad, + compute_MOs_laplacian, + compute_MOs_value_grad_lap, +) if TYPE_CHECKING: # pragma: no cover - typing-only import to avoid circular dependency from .wavefunction import VariationalParameterBlock @@ -518,6 +525,24 @@ def compute_orb_laplacian_api(self) -> Callable[..., npt.NDArray[np.float64]]: else: raise NotImplementedError + @property + def compute_orb_value_grad_lap_api(self) -> Callable[..., tuple]: + """Fused (value, grad, laplacian) api for AOs or MOs. + + Returns a callable that yields ``(val, gx, gy, gz, lap)`` in a single + dispatch — used by the streaming advance hot path so the heavy block + (``exp``, polynomial chain, ``S_l_m``) is shared across val/grad/lap + instead of being recomputed three times. + """ + if isinstance(self.orb_data_up_spin, AOs_sphe_data) and isinstance(self.orb_data_dn_spin, AOs_sphe_data): + return compute_AOs_value_grad_lap + elif isinstance(self.orb_data_up_spin, AOs_cart_data) and isinstance(self.orb_data_dn_spin, AOs_cart_data): + return compute_AOs_value_grad_lap + elif isinstance(self.orb_data_up_spin, MOs_data) and isinstance(self.orb_data_dn_spin, MOs_data): + return compute_MOs_value_grad_lap + else: + raise NotImplementedError + def to_cartesian(self) -> "Geminal_data": """Convert spherical orbitals to Cartesian and transform the lambda matrix. @@ -1363,9 +1388,9 @@ def _compute_geminal_all_elements( lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) - # orb_matrix_* may be produced in the orb_eval zone (potentially float32). - # Explicitly upcast to the geminal zone here so the matmul does not rely - # on JAX implicit type promotion (fp32 x fp64 -> fp64). + # orb_matrix_* may be produced in the ao_eval / mo_eval zone (potentially + # float32). Explicitly upcast to the geminal zone here so the matmul does + # not rely on JAX implicit type promotion (fp32 x fp64 -> fp64). orb_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) orb_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) @@ -1433,8 +1458,8 @@ def compute_geminal_up_one_row_elements( # - up: single position -> 1D vector (n_orb_up,) # - dn: batched positions -> (n_orb_dn, N_dn) # Explicitly upcast to the geminal zone (compute_orb_api may return - # orb_eval dtype, e.g. fp32 for AGP) to avoid relying on JAX implicit - # type promotion in the lambda matmul below. + # ao_eval / mo_eval dtype, e.g. fp32 for AGP) to avoid relying on JAX + # implicit type promotion in the lambda matmul below. orb_up_vec = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_cart).astype(dtype_jnp) orb_up_vec = jnp.reshape(orb_up_vec, (-1,)) # ensure (n_orb_up,) orb_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) @@ -1484,8 +1509,8 @@ def compute_geminal_dn_one_column_elements( # - up: batched positions -> (n_orb_up, N_up) # - dn: single position -> 1D vector (n_orb_dn,) # Explicitly upcast to the geminal zone (compute_orb_api may return - # orb_eval dtype, e.g. fp32 for AGP) to avoid relying on JAX implicit - # type promotion in the lambda matmul below. + # ao_eval / mo_eval dtype, e.g. fp32 for AGP) to avoid relying on JAX + # implicit type promotion in the lambda matmul below. orb_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) orb_matrix_up = jnp.asarray(orb_matrix_up, dtype=dtype_jnp) # (n_orb_up, N_up) @@ -1581,7 +1606,7 @@ def _compute_ratio_determinant_part_rank1_update( lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) # Precompute old AO matrices once. Explicitly upcast to the geminal zone - # (compute_orb_api may return orb_eval dtype, e.g. fp32 for AGP) to avoid + # (compute_orb_api may return ao_eval / mo_eval dtype, e.g. fp32 for AGP) to avoid # relying on JAX implicit type promotion in the lambda matmuls below. orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts).astype(dtype_jnp) orb_matrix_dn_old = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, old_r_dn_carts).astype(dtype_jnp) @@ -1686,7 +1711,7 @@ def _compute_ratio_determinant_part_split_spin( lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) # Precompute old AO matrices once. Explicitly upcast to the geminal zone - # (compute_orb_api may return orb_eval dtype, e.g. fp32 for AGP) to avoid + # (compute_orb_api may return ao_eval / mo_eval dtype, e.g. fp32 for AGP) to avoid # relying on JAX implicit type promotion in the lambda matmuls below. orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts).astype(dtype_jnp) orb_matrix_dn_old = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, old_r_dn_carts).astype(dtype_jnp) @@ -1843,7 +1868,7 @@ def compute_grads_and_laplacian_ln_Det( lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) # Explicitly upcast AO/MO forward values to the kinetic zone - # (compute_orb_api may return orb_eval dtype, e.g. fp32 for AGP) to avoid + # (compute_orb_api may return ao_eval / mo_eval dtype, e.g. fp32 for AGP) to avoid # relying on JAX implicit type promotion in the lambda/gradient matmuls below. ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) @@ -1932,19 +1957,18 @@ def _grads_lap_body( lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) # Explicitly upcast AO/MO forward values to the kinetic zone - # (compute_orb_api may return orb_eval dtype, e.g. fp32 for AGP) to avoid + # (compute_orb_api may return ao_eval / mo_eval dtype, e.g. fp32 for AGP) to avoid # relying on JAX implicit type promotion in the lambda/gradient matmuls below. - ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) - ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) - - ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z = geminal_data.compute_orb_grad_api( - geminal_data.orb_data_up_spin, r_up_carts + # Single fused dispatch shares the heavy block (exp / poly / S_l_m) across + # val/grad/lap. + ao_matrix_up, ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z, ao_matrix_laplacian_up = ( + geminal_data.compute_orb_value_grad_lap_api(geminal_data.orb_data_up_spin, r_up_carts) ) - ao_matrix_dn_grad_x, ao_matrix_dn_grad_y, ao_matrix_dn_grad_z = geminal_data.compute_orb_grad_api( - geminal_data.orb_data_dn_spin, r_dn_carts + ao_matrix_dn, ao_matrix_dn_grad_x, ao_matrix_dn_grad_y, ao_matrix_dn_grad_z, ao_matrix_laplacian_dn = ( + geminal_data.compute_orb_value_grad_lap_api(geminal_data.orb_data_dn_spin, r_dn_carts) ) - ao_matrix_laplacian_up = geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_up_spin, r_up_carts) - ao_matrix_laplacian_dn = geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_dn_spin, r_dn_carts) + ao_matrix_up = ao_matrix_up.astype(dtype_jnp) + ao_matrix_dn = ao_matrix_dn.astype(dtype_jnp) ao_up_grads = jnp.stack([ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z], axis=0) ao_dn_grads = jnp.stack([ao_matrix_dn_grad_x, ao_matrix_dn_grad_y, ao_matrix_dn_grad_z], axis=0) @@ -2130,19 +2154,18 @@ def compute_grads_and_laplacian_ln_Det_fast( lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) # Explicitly upcast AO/MO forward values to the kinetic zone - # (compute_orb_api may return orb_eval dtype, e.g. fp32 for AGP) to avoid + # (compute_orb_api may return ao_eval / mo_eval dtype, e.g. fp32 for AGP) to avoid # relying on JAX implicit type promotion in the lambda/gradient matmuls below. - ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) - ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) - - ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z = geminal_data.compute_orb_grad_api( - geminal_data.orb_data_up_spin, r_up_carts + # Single fused dispatch shares the heavy block (exp / poly / S_l_m) across + # val/grad/lap. + ao_matrix_up, ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z, ao_matrix_laplacian_up = ( + geminal_data.compute_orb_value_grad_lap_api(geminal_data.orb_data_up_spin, r_up_carts) ) - ao_matrix_dn_grad_x, ao_matrix_dn_grad_y, ao_matrix_dn_grad_z = geminal_data.compute_orb_grad_api( - geminal_data.orb_data_dn_spin, r_dn_carts + ao_matrix_dn, ao_matrix_dn_grad_x, ao_matrix_dn_grad_y, ao_matrix_dn_grad_z, ao_matrix_laplacian_dn = ( + geminal_data.compute_orb_value_grad_lap_api(geminal_data.orb_data_dn_spin, r_dn_carts) ) - ao_matrix_laplacian_up = geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_up_spin, r_up_carts) - ao_matrix_laplacian_dn = geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_dn_spin, r_dn_carts) + ao_matrix_up = ao_matrix_up.astype(dtype_jnp) + ao_matrix_dn = ao_matrix_dn.astype(dtype_jnp) ao_up_grads = jnp.stack([ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z], axis=0) ao_dn_grads = jnp.stack([ao_matrix_dn_grad_x, ao_matrix_dn_grad_y, ao_matrix_dn_grad_z], axis=0) @@ -2297,17 +2320,20 @@ def _init_grads_laplacian_ln_Det_streaming_state( lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) # Phase 0: AO/grad/lap evaluation (forward r_*_carts unchanged so the - # underlying kernels reconstruct r-R in fp64 — Principle 3b). - ao_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) - ao_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) - - ao_up_gx, ao_up_gy, ao_up_gz = geminal_data.compute_orb_grad_api(geminal_data.orb_data_up_spin, r_up_carts) - ao_dn_gx, ao_dn_gy, ao_dn_gz = geminal_data.compute_orb_grad_api(geminal_data.orb_data_dn_spin, r_dn_carts) + # underlying kernels reconstruct r-R in fp64 — Principle 3b). Single fused + # dispatch shares the heavy block (exp / poly / S_l_m) across val/grad/lap. + ao_up, ao_up_gx, ao_up_gy, ao_up_gz, ao_up_lap = geminal_data.compute_orb_value_grad_lap_api( + geminal_data.orb_data_up_spin, r_up_carts + ) + ao_dn, ao_dn_gx, ao_dn_gy, ao_dn_gz, ao_dn_lap = geminal_data.compute_orb_value_grad_lap_api( + geminal_data.orb_data_dn_spin, r_dn_carts + ) + ao_up = ao_up.astype(dtype_jnp) + ao_dn = ao_dn.astype(dtype_jnp) ao_up_grads = jnp.asarray(jnp.stack([ao_up_gx, ao_up_gy, ao_up_gz], axis=0), dtype=dtype_jnp) ao_dn_grads = jnp.asarray(jnp.stack([ao_dn_gx, ao_dn_gy, ao_dn_gz], axis=0), dtype=dtype_jnp) - - ao_up_lap = jnp.asarray(geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_up_spin, r_up_carts), dtype=dtype_jnp) - ao_dn_lap = jnp.asarray(geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_dn_spin, r_dn_carts), dtype=dtype_jnp) + ao_up_lap = jnp.asarray(ao_up_lap, dtype=dtype_jnp) + ao_dn_lap = jnp.asarray(ao_dn_lap, dtype=dtype_jnp) # Phase 1: λ_p ⨯ ao_dn family (depends on dn only). paired_dn = lambda_matrix_paired @ ao_dn @@ -2401,17 +2427,13 @@ def _advance_grads_laplacian_ln_Det_streaming_state( def _branch_up(_): # --- Phase 0: single-point AO eval at r_up_new[k] ----------------- + # Single fused dispatch shares the heavy block (exp / poly / S_l_m) + # across val/grad/lap — replaces three separate compute_orb_* calls. r_new = jnp.expand_dims(r_up_carts_new[moved_index], axis=0) # (1, 3) - ao_col = jnp.asarray( - geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_new)[:, 0], - dtype=dtype_jnp, - ) # (n_ao_up,) - gx, gy, gz = geminal_data.compute_orb_grad_api(geminal_data.orb_data_up_spin, r_new) + ao_v, gx, gy, gz, ao_lap = geminal_data.compute_orb_value_grad_lap_api(geminal_data.orb_data_up_spin, r_new) + ao_col = jnp.asarray(ao_v[:, 0], dtype=dtype_jnp) # (n_ao_up,) grad_col = jnp.asarray(jnp.stack([gx[:, 0], gy[:, 0], gz[:, 0]], axis=0), dtype=dtype_jnp) # (3, n_ao_up) - lap_col = jnp.asarray( - geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_up_spin, r_new)[:, 0], - dtype=dtype_jnp, - ) # (n_ao_up,) + lap_col = jnp.asarray(ao_lap[:, 0], dtype=dtype_jnp) # (n_ao_up,) new_ao_up = state.ao_up.at[:, moved_index].set(ao_col) new_ao_up_grads = state.ao_up_grads.at[:, :, moved_index].set(grad_col) @@ -2469,17 +2491,12 @@ def _branch_up(_): def _branch_dn(_): # --- Phase 0: single-point AO eval at r_dn_new[k] ----------------- + # Single fused dispatch (see _branch_up). r_new = jnp.expand_dims(r_dn_carts_new[moved_index], axis=0) - ao_col = jnp.asarray( - geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_new)[:, 0], - dtype=dtype_jnp, - ) # (n_ao_dn,) - gx, gy, gz = geminal_data.compute_orb_grad_api(geminal_data.orb_data_dn_spin, r_new) + ao_v, gx, gy, gz, ao_lap = geminal_data.compute_orb_value_grad_lap_api(geminal_data.orb_data_dn_spin, r_new) + ao_col = jnp.asarray(ao_v[:, 0], dtype=dtype_jnp) # (n_ao_dn,) grad_col = jnp.asarray(jnp.stack([gx[:, 0], gy[:, 0], gz[:, 0]], axis=0), dtype=dtype_jnp) # (3, n_ao_dn) - lap_col = jnp.asarray( - geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_dn_spin, r_new)[:, 0], - dtype=dtype_jnp, - ) # (n_ao_dn,) + lap_col = jnp.asarray(ao_lap[:, 0], dtype=dtype_jnp) # (n_ao_dn,) new_ao_dn = state.ao_dn.at[:, moved_index].set(ao_col) new_ao_dn_grads = state.ao_dn_grads.at[:, :, moved_index].set(grad_col) diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index dd64226a..69fa748c 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -72,8 +72,15 @@ compute_AOs, compute_AOs_grad, compute_AOs_laplacian, + compute_AOs_value_grad_lap, +) +from .molecular_orbital import ( + MOs_data, + compute_MOs, + compute_MOs_grad, + compute_MOs_laplacian, + compute_MOs_value_grad_lap, ) -from .molecular_orbital import MOs_data, compute_MOs, compute_MOs_grad, compute_MOs_laplacian from .structure import Structure_data if TYPE_CHECKING: # typing-only import to avoid circular dependency @@ -3510,8 +3517,21 @@ def _j2_pair_terms(j2b_type: str, a: jax.Array, eps: jax.Array, diff: jax.Array) Mirrors the closures inside ``compute_grads_and_laplacian_Jastrow_two_body`` so init and advance share the exact arithmetic. ``j2b_type`` is JIT-static (Jastrow_two_body_data marks it ``pytree_node=False``). + + Callers may construct ``diff`` in caller-supplied precision (e.g. fp64 + walker coords for ``r - r_new``); cast at the arithmetic use site here so + pair-term outputs always live in this function's own zone + (``jastrow_grad_lap``) regardless of input dtype (Principle 3b — and + required for fori_loop carry-shape stability under mixed precision, + where state.r_up_carts is stored in fp64 but state.grad_J2_up lives in + the grad/lap zone). The cast target is fetched via + ``get_dtype_jnp("jastrow_grad_lap")`` rather than reading ``a.dtype``, + so the function declares its own zone explicitly instead of inheriting + it from a caller-supplied argument. """ - r = jnp.sqrt(jnp.sum(diff * diff, axis=-1)) + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + d = diff.astype(dtype_jnp) + r = jnp.sqrt(jnp.sum(d * d, axis=-1)) r = jnp.maximum(r, eps) if j2b_type == "pade": denom = 1.0 + a * r @@ -3525,7 +3545,7 @@ def _j2_pair_terms(j2b_type: str, a: jax.Array, eps: jax.Array, diff: jax.Array) lap = -(a / 2.0) * exp_term + (2.0 * f_prime) / r else: raise ValueError(f"Unknown jastrow_2b_type: {j2b_type}") - return grad_coeff[..., None] * diff, lap + return grad_coeff[..., None] * d, lap @jit @@ -3534,12 +3554,21 @@ def _init_grads_laplacian_Jastrow_two_body_streaming_state( r_up_carts: jax.Array, r_dn_carts: jax.Array, ) -> Jastrow_two_body_streaming_state: - """Build a J2 state at ``(r_up, r_dn)`` via the existing fresh kernel.""" + """Build a J2 state at ``(r_up, r_dn)`` via the existing fresh kernel. + + Stores ``r_up_carts`` / ``r_dn_carts`` in caller-supplied precision + (Principle 3a — no rebind). Under mixed precision the carry-shape + must match what ``advance`` writes back via + ``state.r_*.at[moved_index].set(r_up_carts_new[moved_index])``; + ``r_up_carts_new`` arrives in fp64 (walker state), so the cached + coords must be fp64 too. Diffs are downcast to the jastrow_grad_lap + zone at the arithmetic use site inside ``_j2_pair_terms`` + (Principle 3b). + """ g_up, g_dn, l_up, l_dn = compute_grads_and_laplacian_Jastrow_two_body(jastrow_two_body_data, r_up_carts, r_dn_carts) - dtype_jnp = get_dtype_jnp("jastrow_grad_lap") return Jastrow_two_body_streaming_state( - r_up_carts=jnp.asarray(r_up_carts, dtype=dtype_jnp), - r_dn_carts=jnp.asarray(r_dn_carts, dtype=dtype_jnp), + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, grad_J2_up=g_up, grad_J2_dn=g_dn, lap_J2_up=l_up, @@ -4001,30 +4030,26 @@ def compute_grads_and_laplacian_Jastrow_three_body( orb_data = jastrow_three_body_data.orb_data if isinstance(orb_data, MOs_data): - compute_orb = compute_MOs - compute_orb_grad = compute_MOs_grad - compute_orb_lapl = compute_MOs_laplacian + compute_orb_vgl = compute_MOs_value_grad_lap elif isinstance(orb_data, (AOs_sphe_data, AOs_cart_data)): - compute_orb = compute_AOs - compute_orb_grad = compute_AOs_grad - compute_orb_lapl = compute_AOs_laplacian + compute_orb_vgl = compute_AOs_value_grad_lap else: raise NotImplementedError - # r_*_carts forwarded unchanged to ``compute_orb`` / ``compute_orb_grad`` / - # ``compute_orb_lapl``; do not pre-cast (the AO/MO kernels reconstruct - # ``r - R`` in float64 internally). - aos_up = jnp.asarray(compute_orb(orb_data, r_up_carts), dtype=dtype_jnp) # (n_orb, n_up) - aos_dn = jnp.asarray(compute_orb(orb_data, r_dn_carts), dtype=dtype_jnp) # (n_orb, n_dn) - - grad_up_x, grad_up_y, grad_up_z = compute_orb_grad(orb_data, r_up_carts) - grad_dn_x, grad_dn_y, grad_dn_z = compute_orb_grad(orb_data, r_dn_carts) + # r_*_carts forwarded unchanged to ``compute_orb_vgl``; do not pre-cast + # (the AO/MO kernels reconstruct ``r - R`` in float64 internally). Single + # fused dispatch shares the heavy block (exp / poly / S_l_m) across + # val/grad/lap. + aos_up, grad_up_x, grad_up_y, grad_up_z, lap_up = compute_orb_vgl(orb_data, r_up_carts) + aos_dn, grad_dn_x, grad_dn_y, grad_dn_z, lap_dn = compute_orb_vgl(orb_data, r_dn_carts) + aos_up = jnp.asarray(aos_up, dtype=dtype_jnp) # (n_orb, n_up) + aos_dn = jnp.asarray(aos_dn, dtype=dtype_jnp) # (n_orb, n_dn) grad_up = jnp.stack([grad_up_x, grad_up_y, grad_up_z], axis=-1) # (n_orb, n_up, 3) grad_dn = jnp.stack([grad_dn_x, grad_dn_y, grad_dn_z], axis=-1) # (n_orb, n_dn, 3) - lap_up = jnp.asarray(compute_orb_lapl(orb_data, r_up_carts), dtype=dtype_jnp) # (n_orb, n_up) - lap_dn = jnp.asarray(compute_orb_lapl(orb_data, r_dn_carts), dtype=dtype_jnp) # (n_orb, n_dn) + lap_up = jnp.asarray(lap_up, dtype=dtype_jnp) # (n_orb, n_up) + lap_dn = jnp.asarray(lap_dn, dtype=dtype_jnp) # (n_orb, n_dn) j1_vec = jastrow_three_body_data._j_matrix_jnp[:, -1].astype(dtype_jnp) # (n_orb,) j3_mat = jastrow_three_body_data._j_matrix_jnp[:, :-1].astype(dtype_jnp) # (n_orb, n_orb) @@ -4125,13 +4150,26 @@ def _three_body_orb_apis(jastrow_three_body_data: Jastrow_three_body_data): """Pick the correct orbital evaluation backends (AO or MO). Returned as a Python tuple so callers can dispatch statically (the - orb_data type is JIT-static via the @struct.dataclass). + orb_data type is JIT-static via the @struct.dataclass). Includes the + fused ``(val, gx, gy, gz, lap)`` dispatcher used by the streaming advance + hot path so the heavy block (exp / poly / S_l_m) is shared across + val/grad/lap. """ orb_data = jastrow_three_body_data.orb_data if isinstance(orb_data, MOs_data): - return compute_MOs, compute_MOs_grad, compute_MOs_laplacian + return ( + compute_MOs, + compute_MOs_grad, + compute_MOs_laplacian, + compute_MOs_value_grad_lap, + ) if isinstance(orb_data, (AOs_sphe_data, AOs_cart_data)): - return compute_AOs, compute_AOs_grad, compute_AOs_laplacian + return ( + compute_AOs, + compute_AOs_grad, + compute_AOs_laplacian, + compute_AOs_value_grad_lap, + ) raise NotImplementedError(f"Unsupported orb_data type: {type(orb_data)}") @@ -4149,20 +4187,19 @@ def _init_grads_laplacian_Jastrow_three_body_streaming_state( """ dtype_jnp = get_dtype_jnp("jastrow_grad_lap") orb_data = jastrow_three_body_data.orb_data - compute_orb, compute_orb_grad, compute_orb_lapl = _three_body_orb_apis(jastrow_three_body_data) + compute_orb, compute_orb_grad, compute_orb_lapl, compute_orb_vgl = _three_body_orb_apis(jastrow_three_body_data) # AO/MO tables (forward r_*_carts unchanged so the underlying kernels can - # reconstruct r-R in float64 — Principle 3b). - aos_up = jnp.asarray(compute_orb(orb_data, r_up_carts), dtype=dtype_jnp) - aos_dn = jnp.asarray(compute_orb(orb_data, r_dn_carts), dtype=dtype_jnp) - - grad_up_x, grad_up_y, grad_up_z = compute_orb_grad(orb_data, r_up_carts) - grad_dn_x, grad_dn_y, grad_dn_z = compute_orb_grad(orb_data, r_dn_carts) + # reconstruct r-R in float64 — Principle 3b). Single fused dispatch shares + # the heavy block (exp / poly / S_l_m) across val/grad/lap. + aos_up, grad_up_x, grad_up_y, grad_up_z, lap_aos_up = compute_orb_vgl(orb_data, r_up_carts) + aos_dn, grad_dn_x, grad_dn_y, grad_dn_z, lap_aos_dn = compute_orb_vgl(orb_data, r_dn_carts) + aos_up = jnp.asarray(aos_up, dtype=dtype_jnp) + aos_dn = jnp.asarray(aos_dn, dtype=dtype_jnp) grad_aos_up = jnp.asarray(jnp.stack([grad_up_x, grad_up_y, grad_up_z], axis=-1), dtype=dtype_jnp) grad_aos_dn = jnp.asarray(jnp.stack([grad_dn_x, grad_dn_y, grad_dn_z], axis=-1), dtype=dtype_jnp) - - lap_aos_up = jnp.asarray(compute_orb_lapl(orb_data, r_up_carts), dtype=dtype_jnp) - lap_aos_dn = jnp.asarray(compute_orb_lapl(orb_data, r_dn_carts), dtype=dtype_jnp) + lap_aos_up = jnp.asarray(lap_aos_up, dtype=dtype_jnp) + lap_aos_dn = jnp.asarray(lap_aos_dn, dtype=dtype_jnp) j_matrix = jastrow_three_body_data._j_matrix_jnp.astype(dtype_jnp) j1_vec = j_matrix[:, -1] @@ -4241,7 +4278,7 @@ def _advance_grads_laplacian_Jastrow_three_body_streaming_state( """ dtype_jnp = get_dtype_jnp("jastrow_grad_lap") orb_data = jastrow_three_body_data.orb_data - compute_orb, compute_orb_grad, compute_orb_lapl = _three_body_orb_apis(jastrow_three_body_data) + compute_orb, compute_orb_grad, compute_orb_lapl, compute_orb_vgl = _three_body_orb_apis(jastrow_three_body_data) j_matrix = jastrow_three_body_data._j_matrix_jnp.astype(dtype_jnp) j3_mat = j_matrix[:, :-1] @@ -4252,12 +4289,13 @@ def _advance_grads_laplacian_Jastrow_three_body_streaming_state( def _branch_up(_): # Single-point AO eval at the moved electron's new position. # NB: forward r_up_carts_new unchanged (Principle 3b — fp64 r-R - # reconstruction inside the kernels). + # reconstruction inside the kernels). Single fused dispatch shares + # the heavy block (exp / poly / S_l_m) across val/grad/lap. r_new = jnp.expand_dims(r_up_carts_new[moved_index], axis=0) # (1, 3) - aos_new_col = jnp.asarray(compute_orb(orb_data, r_new)[:, 0], dtype=dtype_jnp) - gx, gy, gz = compute_orb_grad(orb_data, r_new) + ao_v, gx, gy, gz, ao_lap = compute_orb_vgl(orb_data, r_new) + aos_new_col = jnp.asarray(ao_v[:, 0], dtype=dtype_jnp) grad_aos_new_col = jnp.asarray(jnp.stack([gx[:, 0], gy[:, 0], gz[:, 0]], axis=-1), dtype=dtype_jnp) - lap_aos_new_col = jnp.asarray(compute_orb_lapl(orb_data, r_new)[:, 0], dtype=dtype_jnp) + lap_aos_new_col = jnp.asarray(ao_lap[:, 0], dtype=dtype_jnp) delta_aos = aos_new_col - state.aos_up[:, moved_index] d_J = j3_mat @ delta_aos # (n_orb,) @@ -4310,11 +4348,12 @@ def _branch_up(_): ) def _branch_dn(_): + # Single fused dispatch (see _branch_up). r_new = jnp.expand_dims(r_dn_carts_new[moved_index], axis=0) - aos_new_col = jnp.asarray(compute_orb(orb_data, r_new)[:, 0], dtype=dtype_jnp) - gx, gy, gz = compute_orb_grad(orb_data, r_new) + ao_v, gx, gy, gz, ao_lap = compute_orb_vgl(orb_data, r_new) + aos_new_col = jnp.asarray(ao_v[:, 0], dtype=dtype_jnp) grad_aos_new_col = jnp.asarray(jnp.stack([gx[:, 0], gy[:, 0], gz[:, 0]], axis=-1), dtype=dtype_jnp) - lap_aos_new_col = jnp.asarray(compute_orb_lapl(orb_data, r_new)[:, 0], dtype=dtype_jnp) + lap_aos_new_col = jnp.asarray(ao_lap[:, 0], dtype=dtype_jnp) delta_aos = aos_new_col - state.aos_dn[:, moved_index] d_J = j3_mat @ delta_aos diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index 757b16dc..23efbda4 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -5,6 +5,11 @@ - ``mo_grad``: MO gradient (compute_MOs_grad). - ``mo_lap``: MO Laplacian (compute_MOs_laplacian). +The fused :func:`compute_MOs_value_grad_lap` API returns all three at +once and casts each output into its corresponding zone at the matmul +use site. Use it when value, gradient, and Laplacian are all needed at +the same call site; otherwise prefer the standalone APIs. + See :mod:`jqmc._precision` for details. """ @@ -67,6 +72,7 @@ compute_AOs, compute_AOs_grad, compute_AOs_laplacian, + compute_AOs_value_grad_lap, ) # set logger @@ -242,7 +248,7 @@ def compute_MOs(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: Returns: jax.Array: MO values with shape ``(num_mo, N_e)``. """ - # Heavy AO evaluation runs in ``orb_eval`` precision (e.g. fp32 in mixed mode), + # Heavy AO evaluation runs in ``ao_eval`` precision (e.g. fp32 in mixed mode), # but the (small) MO matmul and the returned MO matrix are kept in the # ``determinant`` precision (fp64 by default). This avoids amplifying fp32 # round-off through downstream determinant / kinetic / energy paths while @@ -289,7 +295,7 @@ def compute_MOs_laplacian(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: dtype_jnp = get_dtype_jnp("mo_lap") mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) ao_lap = compute_AOs_laplacian(mos_data.aos_data, r_carts) - # ao_lap lives in the ao_lap zone; cast to mo_lap at the use site + # ao_lap lives in the ao_grad_lap zone; cast to mo_lap at the use site # (Principle 3b — cast operands to this function's own zone immediately # before consuming them as arithmetic operands). return jnp.dot(mo_coefficients, ao_lap.astype(dtype_jnp)) @@ -356,7 +362,7 @@ def compute_MOs_grad( dtype_jnp = get_dtype_jnp("mo_grad") mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z = compute_AOs_grad(mos_data.aos_data, r_carts) - # AO gradient outputs live in the ao_grad zone; cast to mo_grad at the + # AO gradient outputs live in the ao_grad_lap zone; cast to mo_grad at the # use site (Principle 3b — cast operands to this function's own zone immediately # before consuming them as arithmetic operands). mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x.astype(dtype_jnp)) @@ -366,6 +372,49 @@ def compute_MOs_grad( return mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z +@jit +def compute_MOs_value_grad_lap( + mos_data: MOs_data, r_carts: jax.Array +) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array, jax.Array]: + """Fused evaluation of MO values, Cartesian gradients, and Laplacians. + + Calls :func:`compute_AOs_value_grad_lap` once and applies + ``mo_coefficients @ ·`` to each AO output. Each MO output is cast + into its own zone (Principle 3b) at the matmul use site: + + * ``val`` → ``mo_eval`` (fp32 in mixed mode, fp64 in full) + * ``gx`` / ``gy`` / ``gz`` → ``mo_grad`` (fp64) + * ``lap`` → ``mo_lap`` (fp64) + + For value-only / grad-only / lap-only call sites, prefer the + standalone APIs (``compute_MOs`` / ``compute_MOs_grad`` / + ``compute_MOs_laplacian``) — JAX DCE does not reliably eliminate + unused outputs across this function's ``@jit`` boundary. + + Returns: + tuple: ``(val, gx, gy, gz, lap)``, each of shape ``(num_mo, N_e)``. + """ + dtype_eval = get_dtype_jnp("mo_eval") + dtype_grad = get_dtype_jnp("mo_grad") + dtype_lap = get_dtype_jnp("mo_lap") + + ao_val, ao_gx, ao_gy, ao_gz, ao_lap = compute_AOs_value_grad_lap(aos_data=mos_data.aos_data, r_carts=r_carts) + # AO outputs live in their own zones (ao_val: ao_eval; ao_g*/ao_lap: ao_grad_lap). + # Cast each into the matching MO zone at the matmul use site (Principle 3b). + C = mos_data._mo_coefficients_jnp + C_eval = C.astype(dtype_eval) + C_grad = C.astype(dtype_grad) + C_lap = C.astype(dtype_lap) + + mo_val = jnp.dot(C_eval, ao_val.astype(dtype_eval)) + mo_gx = jnp.dot(C_grad, ao_gx.astype(dtype_grad)) + mo_gy = jnp.dot(C_grad, ao_gy.astype(dtype_grad)) + mo_gz = jnp.dot(C_grad, ao_gz.astype(dtype_grad)) + mo_lap = jnp.dot(C_lap, ao_lap.astype(dtype_lap)) + + return mo_val, mo_gx, mo_gy, mo_gz, mo_lap + + @jit def _compute_MOs_grad_autodiff( mos_data: MOs_data, r_carts: npt.NDArray[np.float64] @@ -378,7 +427,7 @@ def _compute_MOs_grad_autodiff( dtype_jnp = get_dtype_jnp("mo_grad") mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z = _compute_AOs_grad_autodiff(mos_data.aos_data, r_carts) - # AO gradient outputs live in the ao_grad zone; cast to mo_grad at the + # AO gradient outputs live in the ao_grad_lap zone; cast to mo_grad at the # use site (Principle 3b). mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x.astype(dtype_jnp)) mo_matrix_grad_y = jnp.dot(mo_coefficients, mo_matrix_grad_y.astype(dtype_jnp)) diff --git a/jqmc/wavefunction.py b/jqmc/wavefunction.py index 11f7c0cd..4fd055da 100644 --- a/jqmc/wavefunction.py +++ b/jqmc/wavefunction.py @@ -1320,6 +1320,17 @@ def _init_kinetic_energy_all_elements_streaming_state( r_dn_carts=r_dn_carts, ) + # Cast totals to the jastrow_grad_lap zone so init and advance store + # ``grad_J_*`` / ``lap_J_*`` in the same dtype (Principle 3b — required + # for fori_loop carry-shape stability under mixed precision, where + # ``advance`` reassembles the totals from streaming sub-states that + # live in the jastrow_grad_lap zone). + dtype_jnp = get_dtype_jnp("jastrow_grad_lap") + grad_J_up = jnp.asarray(grad_J_up, dtype=dtype_jnp) + grad_J_dn = jnp.asarray(grad_J_dn, dtype=dtype_jnp) + lap_J_up = jnp.asarray(lap_J_up, dtype=dtype_jnp) + lap_J_dn = jnp.asarray(lap_J_dn, dtype=dtype_jnp) + # Determinant streaming state — drives grad_ln_D_*/lap_ln_D_* fields. det_state = _init_grads_laplacian_ln_Det_streaming_state( geminal_data=wavefunction_data.geminal_data, diff --git a/tests/test_AOs.py b/tests/test_AOs.py index de412595..e1c6a221 100755 --- a/tests/test_AOs.py +++ b/tests/test_AOs.py @@ -33,6 +33,7 @@ # POSSIBILITY OF SUCH DAMAGE. import itertools +import os import sys from pathlib import Path @@ -63,12 +64,14 @@ _compute_overlap_matrix_debug, _compute_S_l_m, _compute_S_l_m_debug, - # compute_AOs, + compute_AOs, compute_AOs_grad, compute_AOs_laplacian, + compute_AOs_value_grad_lap, compute_overlap_matrix, ) -from jqmc._precision import get_tolerance # noqa: E402 +from jqmc._precision import get_dtype_jnp, get_tolerance, get_tolerance_min # noqa: E402 +from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 from jqmc.structure import Structure_data # noqa: E402 # JAX float64 @@ -504,7 +507,9 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_auto(): gx_auto, gy_auto, gz_auto = _compute_AOs_grad_autodiff(aos_data=aos_data, r_carts=r_carts) gx_an, gy_an, gz_an = compute_AOs_grad(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("ao_grad", "strict") + # autodiff path goes through compute_AOs (ao_eval zone, fp32 in mixed mode); + # tolerance bottlenecked by ao_eval, not ao_grad_lap. + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "strict") assert not np.any(np.isnan(np.asarray(gx_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_auto))), "NaN detected in second argument" np.testing.assert_allclose(gx_an, gx_auto, atol=atol, rtol=rtol) @@ -567,7 +572,9 @@ def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): gx_auto_cart, gy_auto_cart, gz_auto_cart = _compute_AOs_grad_autodiff(aos_data=aos_data_cart, r_carts=r_carts) gx_num_cart, gy_num_cart, gz_num_cart = _compute_AOs_grad_debug(aos_data=aos_data_cart, r_carts=r_carts) - atol, rtol = get_tolerance("ao_grad", "loose") + # autodiff and FD-debug both pass through compute_AOs (ao_eval zone, fp32 in + # mixed mode); tolerance bottlenecked by ao_eval. + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "loose") assert not np.any(np.isnan(np.asarray(gx_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(gx_auto_cart, gx_num_cart, atol=atol, rtol=rtol) @@ -755,7 +762,9 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_numerical(): gx_num_cart, gy_num_cart, gz_num_cart = _compute_AOs_grad_debug(aos_data=aos_data, r_carts=r_carts) gx_an_cart, gy_an_cart, gz_an_cart = compute_AOs_grad(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("ao_grad", "loose") + # FD-debug path goes through compute_AOs (ao_eval zone, fp32 in mixed mode); + # tolerance bottlenecked by ao_eval. + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "loose") assert not np.any(np.isnan(np.asarray(gx_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(gx_an_cart, gx_num_cart, atol=atol, rtol=rtol) @@ -867,7 +876,9 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_auto(): lap_auto_cart = _compute_AOs_laplacian_autodiff(aos_data=aos_data, r_carts=r_carts) lap_an_cart = compute_AOs_laplacian(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("ao_lap", "strict") + # autodiff path goes through compute_AOs (ao_eval zone, fp32 in mixed mode); + # tolerance bottlenecked by ao_eval, not ao_grad_lap. + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "strict") assert not np.any(np.isnan(np.asarray(lap_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_auto_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_an_cart, lap_auto_cart, atol=atol, rtol=rtol) @@ -965,7 +976,9 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_numerical(): lap_num_cart = _compute_AOs_laplacian_debug(aos_data=aos_data, r_carts=r_carts) lap_an_cart = compute_AOs_laplacian(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("ao_lap", "loose") + # FD-debug path goes through compute_AOs (ao_eval zone, fp32 in mixed mode); + # tolerance bottlenecked by ao_eval. + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "loose") assert not np.any(np.isnan(np.asarray(lap_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_an_cart, lap_num_cart, atol=atol, rtol=rtol) @@ -1065,7 +1078,9 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): lap_num_cart = _compute_AOs_laplacian_autodiff(aos_data=aos_data, r_carts=r_carts) lap_auto_cart = _compute_AOs_laplacian_debug(aos_data=aos_data, r_carts=r_carts) - atol, rtol = get_tolerance("ao_lap", "loose") + # both autodiff and FD-debug go through compute_AOs (ao_eval zone, fp32 in + # mixed mode); tolerance bottlenecked by ao_eval. + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "loose") assert not np.any(np.isnan(np.asarray(lap_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_auto_cart, lap_num_cart, atol=atol, rtol=rtol) @@ -1176,6 +1191,101 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): jax.clear_caches() +@pytest.mark.parametrize( + "trexio_file", + [ + "water_ccecp_ccpvqz.h5", # spherical (l up to f) + "H2_ae_ccpvdz_cart.h5", # Cartesian + "N_ae_ccpvdz_cart.h5", # Cartesian, larger + ], +) +def test_fused_AOs_value_grad_lap_matches_split(trexio_file: str): + """Fused ``compute_AOs_value_grad_lap`` matches the standalone APIs. + + grad/lap parity is bitwise (rtol=atol=0) because the fused kernel + mirrors the standalone grad/lap kernels' shared body verbatim. value + parity is only bounded by the multiplication ordering between the + fused kernel (which reuses the grad/lap ``phi``) and the standalone + ``compute_AOs`` (which builds the polynomial separately) — a few ULPs + of ``ao_eval``-zone precision are allowed. + """ + ( + _structure, + aos_data, + *_rest, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + aos_data.sanity_check() + + rng = np.random.default_rng(20260430) + r_carts = (rng.standard_normal((6, 3)) * 1.5).astype(np.float64) + + val_f, gx_f, gy_f, gz_f, lap_f = compute_AOs_value_grad_lap(aos_data=aos_data, r_carts=r_carts) + val_s = compute_AOs(aos_data=aos_data, r_carts=r_carts) + gx_s, gy_s, gz_s = compute_AOs_grad(aos_data=aos_data, r_carts=r_carts) + lap_s = compute_AOs_laplacian(aos_data=aos_data, r_carts=r_carts) + + # NaN guards. + for arr in (val_f, gx_f, gy_f, gz_f, lap_f, val_s, gx_s, gy_s, gz_s, lap_s): + assert not np.any(np.isnan(np.asarray(arr))) + + # Strict bitwise parity for grad/lap: fused and standalone share the + # exact same expression (same multiplication order, same eps offsets). + assert_allclose(np.asarray(gx_f), np.asarray(gx_s), atol=0, rtol=0) + assert_allclose(np.asarray(gy_f), np.asarray(gy_s), atol=0, rtol=0) + assert_allclose(np.asarray(gz_f), np.asarray(gz_s), atol=0, rtol=0) + assert_allclose(np.asarray(lap_f), np.asarray(lap_s), atol=0, rtol=0) + + # value: tight ao_eval-zone tolerance. Fused reuses the grad/lap + # ``phi`` (left-to-right multiplication chain), while standalone + # ``compute_AOs`` parenthesises the polynomial separately — strictly + # ULP-level differences are allowed. + atol_val, rtol_val = get_tolerance("ao_eval", "strict") + assert_allclose(np.asarray(val_f), np.asarray(val_s), atol=atol_val, rtol=rtol_val) + + jax.clear_caches() + + +@pytest.mark.parametrize( + "trexio_file", + [ + "water_ccecp_ccpvqz.h5", + "H2_ae_ccpvdz_cart.h5", + ], +) +def test_fused_AOs_dtypes_match_zones(trexio_file: str): + """``compute_AOs_value_grad_lap`` outputs are pinned to their zones. + + val ↔ ``ao_eval`` (fp32 mixed / fp64 full); gx/gy/gz/lap ↔ + ``ao_grad_lap`` (fp64 always). + """ + ( + _structure, + aos_data, + *_rest, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + aos_data.sanity_check() + + rng = np.random.default_rng(20260430) + r_carts = (rng.standard_normal((4, 3)) * 1.5).astype(np.float64) + val_f, gx_f, gy_f, gz_f, lap_f = compute_AOs_value_grad_lap(aos_data=aos_data, r_carts=r_carts) + + eval_dtype = get_dtype_jnp("ao_eval") + gradlap_dtype = get_dtype_jnp("ao_grad_lap") + assert val_f.dtype == eval_dtype, f"val.dtype = {val_f.dtype}, expected {eval_dtype}" + assert gx_f.dtype == gradlap_dtype, f"gx.dtype = {gx_f.dtype}, expected {gradlap_dtype}" + assert gy_f.dtype == gradlap_dtype, f"gy.dtype = {gy_f.dtype}, expected {gradlap_dtype}" + assert gz_f.dtype == gradlap_dtype, f"gz.dtype = {gz_f.dtype}, expected {gradlap_dtype}" + assert lap_f.dtype == gradlap_dtype, f"lap.dtype = {lap_f.dtype}, expected {gradlap_dtype}" + + jax.clear_caches() + + @pytest.mark.numerical_diff def test_overlap_matrix_cart_analytic_vs_numerical_debug(): """Cartesian AO overlap matrix from analytic formula matches numerical integration.""" diff --git a/tests/test_MOs.py b/tests/test_MOs.py index 76589e4f..bc42677c 100755 --- a/tests/test_MOs.py +++ b/tests/test_MOs.py @@ -32,6 +32,7 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import os import sys from pathlib import Path @@ -61,9 +62,11 @@ compute_MOs, compute_MOs_grad, compute_MOs_laplacian, + compute_MOs_value_grad_lap, ) -from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 +from jqmc._precision import get_dtype_jnp, get_tolerance, get_tolerance_min # noqa: E402 from jqmc.structure import Structure_data # noqa: E402 +from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 # JAX float64 jax.config.update("jax_enable_x64", True) @@ -454,8 +457,8 @@ def test_MOs_comparing_analytic_and_auto_grads(): grad_x_auto, grad_y_auto, grad_z_auto = _compute_MOs_grad_autodiff(mos_data=mos_data, r_carts=r_carts) - # Path crosses ao_grad (fp32 in mixed) -> mo_grad (fp64); use min. - atol, rtol = get_tolerance_min(["ao_grad", "mo_grad"], "strict") + # Path crosses ao_grad_lap (fp64) -> mo_grad (fp64); use min. + atol, rtol = get_tolerance_min(["ao_grad_lap", "mo_grad"], "strict") assert not np.any(np.isnan(np.asarray(grad_x_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_x_auto))), "NaN detected in second argument" np.testing.assert_allclose(grad_x_an, grad_x_auto, atol=atol, rtol=rtol) @@ -522,8 +525,8 @@ def test_MOs_comparing_analytic_and_auto_laplacians(): mo_lap_auto = _compute_MOs_laplacian_autodiff(mos_data=mos_data, r_carts=r_carts) - # Path crosses ao_lap (fp64) -> mo_lap (fp64); use min. - atol, rtol = get_tolerance_min(["ao_lap", "mo_lap"], "strict") + # Path crosses ao_grad_lap (fp64) -> mo_lap (fp64); use min. + atol, rtol = get_tolerance_min(["ao_grad_lap", "mo_lap"], "strict") assert not np.any(np.isnan(np.asarray(mo_lap_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_lap_auto))), "NaN detected in second argument" np.testing.assert_allclose(mo_lap_an, mo_lap_auto, atol=atol, rtol=rtol) @@ -603,8 +606,8 @@ def test_MOs_sphe_to_cart(): grad_sphe = compute_MOs_grad(mos_data=mos_sphe, r_carts=r_carts) grad_cart = compute_MOs_grad(mos_data=mos_cart, r_carts=r_carts) - # grad path crosses ao_grad (fp32 in mixed) -> mo_grad. - atol_g, rtol_g = get_tolerance_min(["ao_grad", "mo_grad"], "strict") + # grad path crosses ao_grad_lap (fp64) -> mo_grad. + atol_g, rtol_g = get_tolerance_min(["ao_grad_lap", "mo_grad"], "strict") for g_cart, g_sphe in zip(grad_cart, grad_sphe, strict=True): assert not np.any(np.isnan(np.asarray(g_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(g_sphe))), "NaN detected in second argument" @@ -613,8 +616,8 @@ def test_MOs_sphe_to_cart(): lap_sphe = compute_MOs_laplacian(mos_data=mos_sphe, r_carts=r_carts) lap_cart = compute_MOs_laplacian(mos_data=mos_cart, r_carts=r_carts) - # lap path crosses ao_lap (fp64) -> mo_lap (fp64). - atol_l, rtol_l = get_tolerance_min(["ao_lap", "mo_lap"], "strict") + # lap path crosses ao_grad_lap (fp64) -> mo_lap (fp64). + atol_l, rtol_l = get_tolerance_min(["ao_grad_lap", "mo_lap"], "strict") assert not np.any(np.isnan(np.asarray(lap_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_sphe))), "NaN detected in second argument" np.testing.assert_allclose(lap_cart, lap_sphe, atol=atol_l, rtol=rtol_l) @@ -710,8 +713,8 @@ def test_MOs_cart_to_sphe(): grad_cart = compute_MOs_grad(mos_data=mos_cart, r_carts=r_carts) grad_sphe = compute_MOs_grad(mos_data=mos_sphe, r_carts=r_carts) - # grad path crosses ao_grad (fp32 in mixed) -> mo_grad. - atol_g, rtol_g = get_tolerance_min(["ao_grad", "mo_grad"], "strict") + # grad path crosses ao_grad_lap (fp64) -> mo_grad. + atol_g, rtol_g = get_tolerance_min(["ao_grad_lap", "mo_grad"], "strict") for g_cart, g_sphe in zip(grad_cart, grad_sphe, strict=True): assert not np.any(np.isnan(np.asarray(g_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(g_cart))), "NaN detected in second argument" @@ -720,8 +723,8 @@ def test_MOs_cart_to_sphe(): lap_cart = compute_MOs_laplacian(mos_data=mos_cart, r_carts=r_carts) lap_sphe = compute_MOs_laplacian(mos_data=mos_sphe, r_carts=r_carts) - # lap path crosses ao_lap (fp64) -> mo_lap (fp64). - atol_l, rtol_l = get_tolerance_min(["ao_lap", "mo_lap"], "strict") + # lap path crosses ao_grad_lap (fp64) -> mo_lap (fp64). + atol_l, rtol_l = get_tolerance_min(["ao_grad_lap", "mo_lap"], "strict") assert not np.any(np.isnan(np.asarray(lap_sphe))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_sphe, lap_cart, atol=atol_l, rtol=rtol_l) @@ -729,6 +732,90 @@ def test_MOs_cart_to_sphe(): jax.clear_caches() +@pytest.mark.parametrize( + "trexio_file", + [ + "water_ccecp_ccpvqz.h5", # spherical + "H2_ae_ccpvdz_cart.h5", # Cartesian + "N_ae_ccpvdz_cart.h5", + ], +) +def test_fused_MOs_value_grad_lap_matches_split(trexio_file: str): + """Fused ``compute_MOs_value_grad_lap`` matches the standalone APIs. + + grad/lap parity is bitwise (rtol=atol=0): the fused MO function applies + the same ``mo_coefficients @ ao_*`` matmul as the standalone kernels, + operating on bitwise-identical AO grad/lap arrays produced by the fused + AO kernel. value parity is bounded by the multiplication ordering + inside the fused vs standalone AO eval kernels (a few ULPs of + ``mo_eval``-zone precision). + """ + parsed = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + _structure, _aos, mos_data_up, *_rest = parsed + + rng = np.random.default_rng(20260430) + r_carts = (rng.standard_normal((6, 3)) * 1.5).astype(np.float64) + + val_f, gx_f, gy_f, gz_f, lap_f = compute_MOs_value_grad_lap(mos_data=mos_data_up, r_carts=r_carts) + val_s = compute_MOs(mos_data=mos_data_up, r_carts=r_carts) + gx_s, gy_s, gz_s = compute_MOs_grad(mos_data=mos_data_up, r_carts=r_carts) + lap_s = compute_MOs_laplacian(mos_data=mos_data_up, r_carts=r_carts) + + for arr in (val_f, gx_f, gy_f, gz_f, lap_f, val_s, gx_s, gy_s, gz_s, lap_s): + assert not np.any(np.isnan(np.asarray(arr))) + + # Strict bitwise parity for grad/lap. + np.testing.assert_allclose(np.asarray(gx_f), np.asarray(gx_s), atol=0, rtol=0) + np.testing.assert_allclose(np.asarray(gy_f), np.asarray(gy_s), atol=0, rtol=0) + np.testing.assert_allclose(np.asarray(gz_f), np.asarray(gz_s), atol=0, rtol=0) + np.testing.assert_allclose(np.asarray(lap_f), np.asarray(lap_s), atol=0, rtol=0) + + # value: tight tolerance bottlenecked by mo_eval (and ao_eval, which is + # the same fp32/fp64 zone in mixed/full). + atol_val, rtol_val = get_tolerance_min(["ao_eval", "mo_eval"], "strict") + np.testing.assert_allclose(np.asarray(val_f), np.asarray(val_s), atol=atol_val, rtol=rtol_val) + + jax.clear_caches() + + +@pytest.mark.parametrize( + "trexio_file", + [ + "water_ccecp_ccpvqz.h5", + "H2_ae_ccpvdz_cart.h5", + ], +) +def test_fused_MOs_dtypes_match_zones(trexio_file: str): + """``compute_MOs_value_grad_lap`` outputs are pinned to their zones. + + val ↔ ``mo_eval`` (fp32 mixed / fp64 full); gx/gy/gz ↔ ``mo_grad`` + (fp64); lap ↔ ``mo_lap`` (fp64). + """ + parsed = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + _structure, _aos, mos_data_up, *_rest = parsed + + rng = np.random.default_rng(20260430) + r_carts = (rng.standard_normal((4, 3)) * 1.5).astype(np.float64) + val_f, gx_f, gy_f, gz_f, lap_f = compute_MOs_value_grad_lap(mos_data=mos_data_up, r_carts=r_carts) + + eval_dtype = get_dtype_jnp("mo_eval") + grad_dtype = get_dtype_jnp("mo_grad") + lap_dtype = get_dtype_jnp("mo_lap") + assert val_f.dtype == eval_dtype, f"val.dtype = {val_f.dtype}, expected {eval_dtype}" + assert gx_f.dtype == grad_dtype, f"gx.dtype = {gx_f.dtype}, expected {grad_dtype}" + assert gy_f.dtype == grad_dtype, f"gy.dtype = {gy_f.dtype}, expected {grad_dtype}" + assert gz_f.dtype == grad_dtype, f"gz.dtype = {gz_f.dtype}, expected {grad_dtype}" + assert lap_f.dtype == lap_dtype, f"lap.dtype = {lap_f.dtype}, expected {lap_dtype}" + + jax.clear_caches() + + if __name__ == "__main__": from logging import Formatter, StreamHandler, getLogger diff --git a/tests/test_determinant.py b/tests/test_determinant.py index a35b2d0c..f061d100 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -311,9 +311,9 @@ def _build_sphe_aos_l_le6(rng: np.random.Generator) -> AOs_sphe_data: def test_geminal_sphe_to_cart_AOs_data(): """Round-trip AOs l<=6: spherical→Cartesian keeps geminal values/grads.""" - # Comparison crosses ao_eval/det_eval (values) and ao_grad/ao_lap/det_grad_lap (grads); + # Comparison crosses ao_eval/det_eval (values) and ao_grad_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. - atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad", "ao_lap", "det_grad_lap"), "strict") + atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad_lap", "det_grad_lap"), "strict") rng = np.random.default_rng(321) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -351,9 +351,9 @@ def test_geminal_sphe_to_cart_AOs_data(): def test_geminal_cart_to_sphe_AOs_data(): """Round-trip AOs l<=6: Cartesian→spherical keeps geminal values/grads.""" - # Comparison crosses ao_eval/det_eval (values) and ao_grad/ao_lap/det_grad_lap (grads); + # Comparison crosses ao_eval/det_eval (values) and ao_grad_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. - atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad", "ao_lap", "det_grad_lap"), "strict") + atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad_lap", "det_grad_lap"), "strict") rng = np.random.default_rng(654) aos_sphe = _build_sphe_aos_l_le6(rng) @@ -393,10 +393,10 @@ def test_geminal_cart_to_sphe_AOs_data(): def test_geminal_sphe_to_cart_MOs_data(): """Round-trip MOs built on l<=6 AOs: spherical→Cartesian keeps geminal values/grads.""" - # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad/ao_lap/mo_grad/mo_lap/det_grad_lap (grads); + # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad_lap/mo_grad/mo_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. atol_c, rtol_c = get_tolerance_min( - ("ao_eval", "mo_eval", "det_eval", "ao_grad", "ao_lap", "mo_grad", "mo_lap", "det_grad_lap"), + ("ao_eval", "mo_eval", "det_eval", "ao_grad_lap", "mo_grad", "mo_lap", "det_grad_lap"), "strict", ) rng = np.random.default_rng(777) @@ -440,10 +440,10 @@ def test_geminal_sphe_to_cart_MOs_data(): def test_geminal_cart_to_sphe_MOs_data(): """Round-trip MOs l<=6: Cartesian→spherical keeps geminal values/grads.""" - # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad/ao_lap/mo_grad/mo_lap/det_grad_lap (grads); + # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad_lap/mo_grad/mo_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. atol_c, rtol_c = get_tolerance_min( - ("ao_eval", "mo_eval", "det_eval", "ao_grad", "ao_lap", "mo_grad", "mo_lap", "det_grad_lap"), + ("ao_eval", "mo_eval", "det_eval", "ao_grad_lap", "mo_grad", "mo_lap", "det_grad_lap"), "strict", ) rng = np.random.default_rng(888) @@ -822,7 +822,10 @@ def test_grads_and_laplacian_fast_update(trexio_file: str): r_dn_carts=r_dn_carts, ) - atol, rtol = get_tolerance("det_grad_lap", "strict") + # Debug helper above is autodiff through compute_ln_det → bottlenecked by + # ao_eval (fp32 in mixed mode); fast path is fp64 (ao_grad_lap), so the + # achievable agreement is bounded by ao_eval, not det_grad_lap. + atol, rtol = get_tolerance_min(["ao_eval", "det_grad_lap"], "strict") assert not np.any(np.isnan(np.asarray(grad_up_fast))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_up_debug))), "NaN detected in second argument" np.testing.assert_allclose(grad_up_fast, grad_up_debug, atol=atol, rtol=rtol) @@ -1300,7 +1303,10 @@ def test_analytic_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): r_dn_carts=r_dn_carts, ) - atol, rtol = get_tolerance("det_grad_lap", "strict") + # Auto path is autodiff through compute_ln_det → bottlenecked by ao_eval + # (fp32 in mixed mode); analytic path is fp64 (ao_grad_lap), so achievable + # agreement is bounded by ao_eval, not det_grad_lap. + atol, rtol = get_tolerance_min(["ao_eval", "det_grad_lap"], "strict") assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_analytic)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index 067132df..1bcf6723 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -43,7 +43,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 from jqmc.atomic_orbital import AOs_sphe_data # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -1104,7 +1104,9 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): @pytest.mark.activate_if_skip_heavy def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): """Analytic vs auto-diff gradients/laplacian for three-body Jastrow (AOs).""" - atol, rtol = get_tolerance("jastrow_grad_lap", "strict") + # J3 grad/lap crosses two zones: jastrow_grad_lap (fp32 mixed) + ao_grad_lap (fp64). + # Use the looser of the two — under mixed precision, jastrow_grad_lap dominates. + atol, rtol = get_tolerance_min(["jastrow_grad_lap", "ao_grad_lap"], "strict") num_r_up_cart_samples = 4 num_r_dn_cart_samples = 2 num_R_cart_samples = 5 @@ -1176,7 +1178,9 @@ def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): @pytest.mark.activate_if_skip_heavy def test_analytic_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): """Analytic vs auto-diff gradients/laplacian for three-body Jastrow (MOs).""" - atol, rtol = get_tolerance("jastrow_grad_lap", "strict") + # J3-with-MOs crosses jastrow_grad_lap (fp32 mixed) + ao_grad_lap + mo_grad + mo_lap. + # All non-jastrow zones are fp64; jastrow_grad_lap dominates as the loosest. + atol, rtol = get_tolerance_min(["jastrow_grad_lap", "ao_grad_lap", "mo_grad", "mo_lap"], "strict") num_el = 8 num_mo = 4 num_ao = 3 @@ -1382,7 +1386,9 @@ def test_numerical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): @pytest.mark.parametrize("j1b_type,j2b_type,include_nn", _JASTROW_COMBOS) def test_analytical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): """Analytic vs auto-diff gradients/laplacian for J1+J2+J3(+NN).""" - atol, rtol = get_tolerance("jastrow_grad_lap", "strict") + # Combined J1+J2+J3(+NN) grad/lap crosses jastrow_grad_lap (fp32 mixed) and the + # AO/MO grad/lap zones via the J3 path. jastrow_grad_lap is the loosest under mixed. + atol, rtol = get_tolerance_min(["jastrow_grad_lap", "ao_grad_lap", "mo_grad", "mo_lap"], "strict") jastrow_data, r_up_carts, r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) grad_up_an, grad_dn_an, lap_up_an, lap_dn_an = compute_grads_and_laplacian_Jastrow_part( @@ -1418,7 +1424,10 @@ def test_analytical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): @pytest.mark.parametrize("pattern", ["all_moved", "none_moved", "mixed"]) def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern: str): """Compare ratio Jastrow part: debug vs rank-1 update implementation.""" - atol, rtol = get_tolerance("jastrow_eval", "strict") + # Both _compute_ratio_Jastrow_part_rank1_update and _compute_ratio_Jastrow_part_debug + # operate in the jastrow_ratio zone (J(R')/J(R) log-ratio). Use that zone's tolerance + # to honor the 1-zone-1-module principle. + atol, rtol = get_tolerance("jastrow_ratio", "strict") np.random.seed(0) jastrow_data, old_r_up_carts, old_r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) @@ -1618,7 +1627,11 @@ def test_streaming_J1_state_against_full(j1b_type, n_up, n_dn): ) K = 32 - atol, rtol = get_tolerance("wf_kinetic", "strict") + # J1 streaming exercises only the jastrow_grad_lap zone (electron-nucleus, + # no AO/MO involvement). Tolerance must follow that zone, NOT wf_kinetic + # (the latter is fp64-only and incorrectly tightens the bound under mixed + # precision where jastrow_grad_lap = fp32). + atol, rtol = get_tolerance("jastrow_grad_lap", "strict") for _ in range(K): spin_choices = [] if n_up > 0: @@ -1685,7 +1698,11 @@ def test_streaming_J2_state_against_full(j2b_type, n_up, n_dn): ) K = 32 - atol, rtol = get_tolerance("wf_kinetic", "strict") + # J2 streaming exercises only the jastrow_grad_lap zone (electron-electron + # pair coupling, no AO/MO involvement). Under mixed precision the pair + # delta path additionally accumulates fp32 cancellation error over K steps; + # the jastrow_grad_lap fp32 strict tolerance (1e-5, 1e-3) covers this. + atol, rtol = get_tolerance("jastrow_grad_lap", "strict") for _ in range(K): spin_choices = [] if n_up > 0: @@ -1762,7 +1779,10 @@ def test_streaming_J3_state_against_full(trexio_file): state = _init_grads_laplacian_Jastrow_three_body_streaming_state(jastrow_threebody_data, r_up, r_dn) K = 32 - atol, rtol = get_tolerance("wf_kinetic", "strict") + # J3 streaming crosses two zones: jastrow_grad_lap (J3 grad/lap arithmetic) + # and ao_grad_lap (AO grad/lap consumed inside J3). Use the looser of the + # two — under mixed precision, jastrow_grad_lap (fp32) dominates. + atol, rtol = get_tolerance_min(["jastrow_grad_lap", "ao_grad_lap"], "strict") for _ in range(K): # pick a random single-electron move (alternating spins when available) spin_choices = [] diff --git a/tests/test_mixed_precision.py b/tests/test_mixed_precision.py index e378aede..546a5091 100644 --- a/tests/test_mixed_precision.py +++ b/tests/test_mixed_precision.py @@ -188,16 +188,16 @@ def test_compute_AOs_output_dtype(self, h2_data): ) def test_compute_AOs_grad_output_dtype(self, h2_data): - """compute_AOs_grad must return ao_grad zone dtype.""" + """compute_AOs_grad must return ao_grad_lap zone dtype.""" grad_x, grad_y, grad_z = compute_AOs_grad(h2_data["aos_data"], h2_data["r_up"]) - expected = get_dtype_jnp("ao_grad") + expected = get_dtype_jnp("ao_grad_lap") for name, arr in [("grad_x", grad_x), ("grad_y", grad_y), ("grad_z", grad_z)]: assert arr.dtype == expected, f"compute_AOs_grad {name} dtype is {arr.dtype}, expected {expected}." def test_compute_AOs_laplacian_output_dtype(self, h2_data): - """compute_AOs_laplacian must return ao_lap zone dtype.""" + """compute_AOs_laplacian must return ao_grad_lap zone dtype.""" lap = compute_AOs_laplacian(h2_data["aos_data"], h2_data["r_up"]) - expected = get_dtype_jnp("ao_lap") + expected = get_dtype_jnp("ao_grad_lap") assert lap.dtype == expected, f"compute_AOs_laplacian dtype is {lap.dtype}, expected {expected}." @@ -426,11 +426,11 @@ def test_compute_AOs_sphe_output_dtype(self, h2_sphe_data): def test_compute_AOs_sphe_grad_output_dtype(self, h2_sphe_data): gx, gy, gz = compute_AOs_grad(h2_sphe_data["aos_data"], h2_sphe_data["r_up"]) for name, arr in [("grad_x", gx), ("grad_y", gy), ("grad_z", gz)]: - _assert_dtype(arr, get_dtype_jnp("ao_grad"), f"compute_AOs_grad sphe {name}") + _assert_dtype(arr, get_dtype_jnp("ao_grad_lap"), f"compute_AOs_grad sphe {name}") def test_compute_AOs_sphe_laplacian_output_dtype(self, h2_sphe_data): lap = compute_AOs_laplacian(h2_sphe_data["aos_data"], h2_sphe_data["r_up"]) - _assert_dtype(lap, get_dtype_jnp("ao_lap"), "compute_AOs_laplacian (sphe)") + _assert_dtype(lap, get_dtype_jnp("ao_grad_lap"), "compute_AOs_laplacian (sphe)") class TestMOExtendedDtype: From c8eab12dddecdee4c8eba736e221f72099361e2d Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 1 May 2026 09:36:40 +0900 Subject: [PATCH 21/97] Removed jit from internal functions. --- jqmc/coulomb_potential.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 461d9ccb..68c18f2d 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -1553,7 +1553,12 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( # weight_all = jnp.zeros((0,)) # V_l_mapped_all = jnp.zeros((global_max_ang_mom_plus_1, 0)) - @jit + # NOTE: No `@jit` here — these are micro-ops (a few flops) called via + # `vmap(vmap(...))` inside the parent jit. An inner `@jit` forces XLA to + # cut the parent computation at the function boundary, defeating fusion + # and producing a per-(electron×neighbor×Nv) launch storm in LRDMC + # projection. Keep them as plain Python so the parent jit fuses them + # into the surrounding mesh-build / contract chain. def compute_V_l(rel_R_cart_min_dist, exponents, coefficients, powers): V_l = ( jnp.linalg.norm(rel_R_cart_min_dist) ** -2.0 @@ -1564,7 +1569,6 @@ def compute_V_l(rel_R_cart_min_dist, exponents, coefficients, powers): return V_l - @jit def compute_P_l(ang_mom, cos_theta, weight, wf_ratio): P_l = (2 * ang_mom + 1) * jnp_legendre_tablated(ang_mom, cos_theta) * weight * wf_ratio return P_l From 6bb7c1e52a8740f48ce6e244d5a6b604e5870312 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 1 May 2026 10:07:42 +0900 Subject: [PATCH 22/97] Try to remove the nested vmap. --- jqmc/coulomb_potential.py | 41 +++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 68c18f2d..1e4d7d2d 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -1523,6 +1523,18 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( # RT is also forwarded as-is (Principle 3a); cast at the use site below. dtype_jnp = get_dtype_jnp("coulomb") + # The mesh-build path below uses a batched gather (`jnp.take(positions, ...)`) + # that assumes no minimum-image wrap. PBC is not yet supported by this + # fast-update entry point — fail loudly so a future PBC enablement does + # not silently produce incorrect ECP non-local contributions. + if coulomb_potential_data.structure_data.pbc_flag: + raise ValueError( + "compute_ecp_non_local_parts_nearest_neighbors_fast_update does not " + "support PBC (pbc_flag=True). The batched-gather mesh build skips " + "minimum-image wrapping. Restore the per-electron vmap over " + "_get_min_dist_rel_R_cart_jnp (with its PBC branch) before enabling PBC." + ) + if Nv == 4: weights = jnp.array(tetrahedron_sym_mesh_Nv4.weights, dtype=dtype_jnp) grid_points = jnp.array(tetrahedron_sym_mesh_Nv4.grid_points, dtype=dtype_jnp) @@ -1545,6 +1557,7 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( # jnp variables ang_mom_all, exponent_all, coefficient_all, power_all = coulomb_potential_data._padded_parameters_tuple global_max_ang_mom_plus_1 = coulomb_potential_data._global_max_ang_mom_plus_1 + positions_cart = coulomb_potential_data.structure_data._positions_cart_jnp # (n_atoms, 3), fp64 # stored non_local_ecp_part_r_carts_up = jnp.zeros((0, len(r_up_carts), 3), dtype=dtype_jnp) @@ -1553,12 +1566,7 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( # weight_all = jnp.zeros((0,)) # V_l_mapped_all = jnp.zeros((global_max_ang_mom_plus_1, 0)) - # NOTE: No `@jit` here — these are micro-ops (a few flops) called via - # `vmap(vmap(...))` inside the parent jit. An inner `@jit` forces XLA to - # cut the parent computation at the function boundary, defeating fusion - # and producing a per-(electron×neighbor×Nv) launch storm in LRDMC - # projection. Keep them as plain Python so the parent jit fuses them - # into the surrounding mesh-build / contract chain. + @jit def compute_V_l(rel_R_cart_min_dist, exponents, coefficients, powers): V_l = ( jnp.linalg.norm(rel_R_cart_min_dist) ** -2.0 @@ -1569,6 +1577,7 @@ def compute_V_l(rel_R_cart_min_dist, exponents, coefficients, powers): return V_l + @jit def compute_P_l(ang_mom, cos_theta, weight, wf_ratio): P_l = (2 * ang_mom + 1) * jnp_legendre_tablated(ang_mom, cos_theta) * weight * wf_ratio return P_l @@ -1593,17 +1602,15 @@ def _build_mesh_for_spin(r_carts, other_carts): ) )(r_carts) - def _rels_for_electron(r_cart, i_atom_list): - return vmap( - lambda i_atom: _get_min_dist_rel_R_cart_jnp( - structure_data=coulomb_potential_data.structure_data, - r_cart=r_cart, - i_atom=i_atom, - dtype=dtype_jnp, - ) - )(i_atom_list) - - rels = vmap(_rels_for_electron)(r_carts, i_atom_lists) # (n_spin, NN, 3) + # Vectorised replacement of the nested + # vmap(vmap(_get_min_dist_rel_R_cart_jnp))(r_carts, i_atom_lists) + # which lowered to a per-(electron × NN) `dynamic_slice` loop and was + # the dominant launch source in LRDMC ECP non-local. Single batched + # gather + broadcast subtract = one fused kernel under the parent jit. + # PBC minimum-image is intentionally omitted here — guarded at function + # entry above. Cast to coulomb zone (Principle 3b) at the use site. + R_carts_NN = jnp.take(positions_cart, i_atom_lists, axis=0) # (n_spin, NN, 3) + rels = (R_carts_NN - r_carts[:, None, :]).astype(dtype_jnp) # (n_spin, NN, 3) rel_norm = jnp.linalg.norm(rels, axis=-1, keepdims=True) offsets = rels[..., None, :] + rel_norm[..., None, :] * grid_points[None, None, :, :] updated_carts = r_carts[:, None, None, :] + offsets # (n_spin, NN, Nv, 3) From 53ec4763e4dfb2c25831d5550406d9a8a9294ff9 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 1 May 2026 10:23:02 +0900 Subject: [PATCH 23/97] Revert nested-vmap and inner-jit removal in ECP non-local --- jqmc/coulomb_potential.py | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 1e4d7d2d..461d9ccb 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -1523,18 +1523,6 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( # RT is also forwarded as-is (Principle 3a); cast at the use site below. dtype_jnp = get_dtype_jnp("coulomb") - # The mesh-build path below uses a batched gather (`jnp.take(positions, ...)`) - # that assumes no minimum-image wrap. PBC is not yet supported by this - # fast-update entry point — fail loudly so a future PBC enablement does - # not silently produce incorrect ECP non-local contributions. - if coulomb_potential_data.structure_data.pbc_flag: - raise ValueError( - "compute_ecp_non_local_parts_nearest_neighbors_fast_update does not " - "support PBC (pbc_flag=True). The batched-gather mesh build skips " - "minimum-image wrapping. Restore the per-electron vmap over " - "_get_min_dist_rel_R_cart_jnp (with its PBC branch) before enabling PBC." - ) - if Nv == 4: weights = jnp.array(tetrahedron_sym_mesh_Nv4.weights, dtype=dtype_jnp) grid_points = jnp.array(tetrahedron_sym_mesh_Nv4.grid_points, dtype=dtype_jnp) @@ -1557,7 +1545,6 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( # jnp variables ang_mom_all, exponent_all, coefficient_all, power_all = coulomb_potential_data._padded_parameters_tuple global_max_ang_mom_plus_1 = coulomb_potential_data._global_max_ang_mom_plus_1 - positions_cart = coulomb_potential_data.structure_data._positions_cart_jnp # (n_atoms, 3), fp64 # stored non_local_ecp_part_r_carts_up = jnp.zeros((0, len(r_up_carts), 3), dtype=dtype_jnp) @@ -1602,15 +1589,17 @@ def _build_mesh_for_spin(r_carts, other_carts): ) )(r_carts) - # Vectorised replacement of the nested - # vmap(vmap(_get_min_dist_rel_R_cart_jnp))(r_carts, i_atom_lists) - # which lowered to a per-(electron × NN) `dynamic_slice` loop and was - # the dominant launch source in LRDMC ECP non-local. Single batched - # gather + broadcast subtract = one fused kernel under the parent jit. - # PBC minimum-image is intentionally omitted here — guarded at function - # entry above. Cast to coulomb zone (Principle 3b) at the use site. - R_carts_NN = jnp.take(positions_cart, i_atom_lists, axis=0) # (n_spin, NN, 3) - rels = (R_carts_NN - r_carts[:, None, :]).astype(dtype_jnp) # (n_spin, NN, 3) + def _rels_for_electron(r_cart, i_atom_list): + return vmap( + lambda i_atom: _get_min_dist_rel_R_cart_jnp( + structure_data=coulomb_potential_data.structure_data, + r_cart=r_cart, + i_atom=i_atom, + dtype=dtype_jnp, + ) + )(i_atom_list) + + rels = vmap(_rels_for_electron)(r_carts, i_atom_lists) # (n_spin, NN, 3) rel_norm = jnp.linalg.norm(rels, axis=-1, keepdims=True) offsets = rels[..., None, :] + rel_norm[..., None, :] * grid_points[None, None, :, :] updated_carts = r_carts[:, None, None, :] + offsets # (n_spin, NN, Nv, 3) From d421f23340095548b71d1beec609bc64b79ea145 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 1 May 2026 11:00:36 +0900 Subject: [PATCH 24/97] Removed segment_sum from V_l. --- jqmc/coulomb_potential.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 461d9ccb..cd965b01 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -1620,8 +1620,16 @@ def _rels_for_electron(r_cart, i_atom_list): powers = power_all[i_atom_lists] def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): + # NOTE: previously `jax.ops.segment_sum(V_l_vmapped, ang_mom, + # num_segments=global_max_ang_mom_plus_1)`. Inside the outer + # vmap(vmap(...)) the scatter was lowered to a 1-element-per-iter + # GPU while_loop with batch dim flattened to 4096*32*1*L = + # ~262k iters/call, dominating LRDMC launch storm (see + # work/05nvidia-nsight/analysis.md §13). For small L (typically 2-3 + # for cc-ECP) a masked dense reduce avoids the scatter entirely. V_l_vmapped = compute_V_l(rel, exponent, coefficient, power) - return jax.ops.segment_sum(V_l_vmapped, ang_mom, num_segments=global_max_ang_mom_plus_1) + mask = ang_mom[:, None] == jnp.arange(global_max_ang_mom_plus_1)[None, :] + return jnp.where(mask, V_l_vmapped[:, None], 0.0).sum(axis=0) V_l_mapped = vmap(vmap(_V_l_mapped, in_axes=(0, 0, 0, 0, 0)))(rels, ang_moms, exponents, coefficients, powers) V_l_dup = jnp.repeat(V_l_mapped[:, :, :, None], grid_points.shape[0], axis=3) From 69486608e791ca6c074285c11df7d1e9fe3f9ff1 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 1 May 2026 11:28:27 +0900 Subject: [PATCH 25/97] Totally remove segment_sum from V_l. --- jqmc/coulomb_potential.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index cd965b01..016cfa09 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -1379,8 +1379,13 @@ def _rels_for_electron(r_cart, i_atom_list): powers = power_all[i_atom_lists] def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): + # NOTE: see compute_ecp_non_local_parts_nearest_neighbors_fast_update + # below for the rationale — `jax.ops.segment_sum` inside vmap(vmap(...)) + # lowers to a 4096*32*1*L-iter while_loop on GPU. For small L + # (typically 2-3) a masked dense reduce is dramatically faster. V_l_vmapped = compute_V_l(rel, exponent, coefficient, power) - return jax.ops.segment_sum(V_l_vmapped, ang_mom, num_segments=global_max_ang_mom_plus_1) + mask = ang_mom[:, None] == jnp.arange(global_max_ang_mom_plus_1)[None, :] + return jnp.where(mask, V_l_vmapped[:, None], 0.0).sum(axis=0) V_l_mapped = vmap(vmap(_V_l_mapped, in_axes=(0, 0, 0, 0, 0)))(rels, ang_moms, exponents, coefficients, powers) @@ -1787,8 +1792,15 @@ def compute_ecp_non_local_parts_all_pairs( # start = time.perf_counter() nucleus_index_non_local_part = np.array(coulomb_potential_data._nucleus_index_non_local_part, dtype=np.int32) num_segments = len(set(coulomb_potential_data._nucleus_index_non_local_part)) - V_ecp_up = jax.ops.segment_sum(V_ecp_up, nucleus_index_non_local_part, num_segments=num_segments) - V_ecp_dn = jax.ops.segment_sum(V_ecp_dn, nucleus_index_non_local_part, num_segments=num_segments) + # NOTE: previously two `jax.ops.segment_sum(..., num_segments=num_segments)`. + # Replaced with a masked dense reduce (matmul over the kr-pair axis) so the + # XLA scatter-pathology that bites V_l (see compute_ecp_non_local_parts_nearest_neighbors_fast_update + # and analysis.md §13/§14) cannot resurface here. n_kr_pairs and num_segments + # are both small (~tens), so the dense reduce is essentially free. + nucleus_index_non_local_part_jnp = jnp.asarray(nucleus_index_non_local_part) + _aggregator_mask = nucleus_index_non_local_part_jnp[:, None] == jnp.arange(num_segments)[None, :] + V_ecp_up = jnp.einsum("ks,k...->s...", _aggregator_mask.astype(V_ecp_up.dtype), V_ecp_up) + V_ecp_dn = jnp.einsum("ks,k...->s...", _aggregator_mask.astype(V_ecp_dn.dtype), V_ecp_dn) # end = time.perf_counter() # logger.info(f"Segment sum elapsed Time = {(end-start)*1e3:.3f} msec.") From 5d77ca853d0cc3a0a4432fc096b3d522c27ce5b8 Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Fri, 1 May 2026 16:07:54 +0900 Subject: [PATCH 26/97] Fix streaming KE test tolerances for mixed-precision mode Replace get_tolerance("wf_kinetic", ...) with get_tolerance_min(["wf_kinetic", "jastrow_grad_lap"], ...) in all streaming kinetic energy tests. In mixed mode jastrow_grad_lap is float32, so the achievable agreement is bounded by that zone's tolerance (~1e-5/1e-3), not wf_kinetic's float64 tolerance (1e-8/1e-6). --- tests/test_wave_function.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index cb4b6fbc..2e270d43 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -45,7 +45,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 from jqmc.determinant import compute_geminal_all_elements # noqa: E402 from jqmc.jastrow_factor import ( # noqa: E402 Jastrow_data, @@ -797,7 +797,7 @@ def test_streaming_kinetic_energy_step_consistency(trexio_file): rng = np.random.RandomState(0) r_up0 = 4.0 * rng.rand(n_up, 3) - 2.0 r_dn0 = 4.0 * rng.rand(n_dn, 3) - 2.0 - atol, rtol = get_tolerance("wf_kinetic", "strict") + atol, rtol = get_tolerance_min(["wf_kinetic", "jastrow_grad_lap"], "strict") _streaming_step_consistency_one(wf, r_up0, r_dn0, K=32, atol=atol, rtol=rtol) @@ -810,7 +810,7 @@ def test_streaming_kinetic_drift_accumulation(K): rng = np.random.RandomState(1) r_up0 = 4.0 * rng.rand(gem.num_electron_up, 3) - 2.0 r_dn0 = 4.0 * rng.rand(gem.num_electron_dn, 3) - 2.0 - atol, rtol = get_tolerance("wf_kinetic", "loose") + atol, rtol = get_tolerance_min(["wf_kinetic", "jastrow_grad_lap"], "loose") _streaming_step_consistency_one(wf, r_up0, r_dn0, K=K, atol=atol, rtol=rtol, seed=2) @@ -825,7 +825,7 @@ def test_streaming_kinetic_edge_cases(trexio_file): rng = np.random.RandomState(3) r_up0 = 4.0 * rng.rand(gem.num_electron_up, 3) - 2.0 r_dn0 = 4.0 * rng.rand(gem.num_electron_dn, 3) - 2.0 - atol, rtol = get_tolerance("wf_kinetic", "strict") + atol, rtol = get_tolerance_min(["wf_kinetic", "jastrow_grad_lap"], "strict") _streaming_step_consistency_one(wf, r_up0, r_dn0, K=24, atol=atol, rtol=rtol, seed=4) @@ -839,7 +839,7 @@ def test_streaming_kinetic_jastrow_combinations(jastrow_combo): rng = np.random.RandomState(5) r_up0 = 4.0 * rng.rand(gem.num_electron_up, 3) - 2.0 r_dn0 = 4.0 * rng.rand(gem.num_electron_dn, 3) - 2.0 - atol, rtol = get_tolerance("wf_kinetic", "strict") + atol, rtol = get_tolerance_min(["wf_kinetic", "jastrow_grad_lap"], "strict") _streaming_step_consistency_one(wf, r_up0, r_dn0, K=24, atol=atol, rtol=rtol, seed=6) @@ -893,7 +893,7 @@ def _make_init_state(r_up, r_dn): # Reference: fresh evaluation per walker. ke_up_v, ke_dn_v = jax.vmap(_kinetic_energy_from_streaming_state)(states_new) - atol, rtol = get_tolerance("wf_kinetic", "strict") + atol, rtol = get_tolerance_min(["wf_kinetic", "jastrow_grad_lap"], "strict") for w in range(n_walkers): ke_up_ref, ke_dn_ref = compute_kinetic_energy_all_elements_fast_update( wavefunction_data=wf, From d964a258303fc203ac1758c89a53cc37a07f935c Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Fri, 1 May 2026 16:23:23 +0900 Subject: [PATCH 27/97] fix: replace hessian() with jvp(grad) for NNJastrow Laplacian hessian() builds an O(n^2) computation graph that causes XLA JIT to generate excessively large kernels (e.g. add.31668_kernel) when grad(compute_local_energy) differentiates through compute_kinetic_energy (effectively 3rd-order AD). Replace with forward-over-reverse diagonal Hessian (jvp(grad)) which computes only the n diagonal elements needed for the Laplacian, reducing the graph to O(n). --- jqmc/jastrow_factor.py | 50 +++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 69fa748c..cf613bce 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -3060,11 +3060,27 @@ def _compute_Jastrow_nn_only(r_up, r_dn): grad_JNN_up = grad(_compute_Jastrow_nn_only, argnums=0)(r_up_carts_jnp, r_dn_carts_jnp) grad_JNN_dn = grad(_compute_Jastrow_nn_only, argnums=1)(r_up_carts_jnp, r_dn_carts_jnp) - hessian_JNN_up = hessian(_compute_Jastrow_nn_only, argnums=0)(r_up_carts_jnp, r_dn_carts_jnp) - lap_JNN_up = jnp.einsum("ijij->i", hessian_JNN_up) - - hessian_JNN_dn = hessian(_compute_Jastrow_nn_only, argnums=1)(r_up_carts_jnp, r_dn_carts_jnp) - lap_JNN_dn = jnp.einsum("ijij->i", hessian_JNN_dn) + # Compute per-electron Laplacian via forward-over-reverse (diagonal Hessian only). + # This produces an O(n) computation graph instead of O(n²) from hessian(), + # which significantly reduces the XLA kernel size when grad(compute_local_energy) + # differentiates through the kinetic energy (i.e. under 3rd-order AD). + def _lap_jvp(f_r, r): + """Sum of diagonal Hessian (Laplacian) per electron via jvp(grad).""" + n_elec, n_coord = r.shape + n = n_elec * n_coord + g_r = grad(f_r) + + def diag_one(e_flat): + e = e_flat.reshape(r.shape) + _, jvp_val = jax.jvp(g_r, (r,), (e,)) + return jnp.sum(jvp_val * e) + + basis = jnp.eye(n, dtype=r.dtype) + diags = jax.vmap(diag_one)(basis) + return diags.reshape(n_elec, n_coord).sum(axis=-1) + + lap_JNN_up = _lap_jvp(lambda r: _compute_Jastrow_nn_only(r, r_dn_carts_jnp), r_up_carts_jnp) + lap_JNN_dn = _lap_jvp(lambda r: _compute_Jastrow_nn_only(r_up_carts_jnp, r), r_dn_carts_jnp) grad_J_up = grad_J_up + grad_JNN_up grad_J_dn = grad_J_dn + grad_JNN_dn @@ -3171,11 +3187,25 @@ def _compute_Jastrow_nn_only(r_up, r_dn): grad_JNN_up = grad(_compute_Jastrow_nn_only, argnums=0)(r_up_carts_jnp, r_dn_carts_jnp) grad_JNN_dn = grad(_compute_Jastrow_nn_only, argnums=1)(r_up_carts_jnp, r_dn_carts_jnp) - hessian_JNN_up = hessian(_compute_Jastrow_nn_only, argnums=0)(r_up_carts_jnp, r_dn_carts_jnp) - lap_JNN_up = jnp.einsum("ijij->i", hessian_JNN_up) - - hessian_JNN_dn = hessian(_compute_Jastrow_nn_only, argnums=1)(r_up_carts_jnp, r_dn_carts_jnp) - lap_JNN_dn = jnp.einsum("ijij->i", hessian_JNN_dn) + # Compute per-electron Laplacian via forward-over-reverse (diagonal Hessian only). + # See compute_grads_and_laplacian_Jastrow_part for rationale. + def _lap_jvp(f_r, r): + """Sum of diagonal Hessian (Laplacian) per electron via jvp(grad).""" + n_elec, n_coord = r.shape + n = n_elec * n_coord + g_r = grad(f_r) + + def diag_one(e_flat): + e = e_flat.reshape(r.shape) + _, jvp_val = jax.jvp(g_r, (r,), (e,)) + return jnp.sum(jvp_val * e) + + basis = jnp.eye(n, dtype=r.dtype) + diags = jax.vmap(diag_one)(basis) + return diags.reshape(n_elec, n_coord).sum(axis=-1) + + lap_JNN_up = _lap_jvp(lambda r: _compute_Jastrow_nn_only(r, r_dn_carts_jnp), r_up_carts_jnp) + lap_JNN_dn = _lap_jvp(lambda r: _compute_Jastrow_nn_only(r_up_carts_jnp, r), r_dn_carts_jnp) grad_J_up = grad_J_up + grad_JNN_up grad_J_dn = grad_J_dn + grad_JNN_dn From a96f0fca256662399d60b7c26cd07ed4a04120a4 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 1 May 2026 18:02:43 +0900 Subject: [PATCH 28/97] ao: replace segment_sum with bucketed reduce+gather The AO-side jax.ops.segment_sum at the tail of every _compute_AOs_* kernel produced a scatter-ADD with non-unique indices, which XLA lowered to a sequential while-loop (loop_dynamic_slice_fusion + loop_dynamic_update_slice_fusion + counter). Profiling showed this fallback dominated 20-40% of wall time across every AO-touching benchmark (jastrow, geminal, kinetic, ECP, MCMC, LRDMC), capping each kernel ~10x below roofline peak. Group AOs by contraction depth K once at trace time, then for each unique K emit a single dense reduce_sum over (n_ao_K, K, ...). Concatenate per-K outputs in bucket order and apply one final inverse-permutation gather to recover the original AO ordering. No scatter, no while loop, zero wasted FLOPs vs. the one-hot GEMM alternative. Verified on CPU StableHLO (water-scale, 60 AOs / 183 prims / 256 walkers): scatter ops 2 -> 0, while ops 0 -> 0; post-XLA-CPU compiled scatter ops 20 -> 0. tests/test_AOs.py (65 tests) pass bit-exact (max |diff| = 0). --- jqmc/atomic_orbital.py | 189 ++++++++++++++++++++++++++++++++--------- 1 file changed, 151 insertions(+), 38 deletions(-) diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index af03e7de..e825421c 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -566,6 +566,20 @@ def _orbital_indices_jnp(self) -> jax.Array: """orbital_index.""" return jnp.array(self.orbital_indices, dtype=jnp.int32) + @property + def _prim_groups_by_K(self) -> tuple: + """Group AOs by contraction depth K (primitives per AO). + + Used by :func:`_reduce_primitives_to_aos` to replace + ``segment_sum`` (which falls back to a scatter-add while-loop + in XLA) with a small, fixed number of dense ``reduce_sum`` ops + — one per unique contraction depth K. + + Returns: + tuple of ``(K, ao_idx_np, prim_idx_np, is_identity_perm)``. + """ + return _build_prim_groups_by_K(self.orbital_indices, self.num_ao) + @property def _atomic_center_carts_np(self) -> npt.NDArray[np.float64]: """Atomic positions in cartesian. @@ -1189,6 +1203,20 @@ def _orbital_indices_jnp(self) -> jax.Array: """orbital_index.""" return jnp.array(self.orbital_indices, dtype=jnp.int32) + @property + def _prim_groups_by_K(self) -> tuple: + """Group AOs by contraction depth K (primitives per AO). + + Used by :func:`_reduce_primitives_to_aos` to replace + ``segment_sum`` (which falls back to a scatter-add while-loop + in XLA) with a small, fixed number of dense ``reduce_sum`` ops + — one per unique contraction depth K. + + Returns: + tuple of ``(K, ao_idx_np, prim_idx_np, is_identity_perm)``. + """ + return _build_prim_groups_by_K(self.orbital_indices, self.num_ao) + @property def _atomic_center_carts_np(self) -> npt.NDArray[np.float64]: """Atomic positions in cartesian. @@ -2004,6 +2032,109 @@ def _compute_AOs_cart_debug(aos_data: AOs_cart_data, r_carts: npt.NDArray[np.flo return aos_values +def _build_prim_groups_by_K(orbital_indices, num_ao: int) -> tuple: + """Group AOs by contraction depth K (number of primitives per AO). + + This is a pure-Python (numpy) preprocessor invoked at trace time + by the cached property :attr:`AOs_cart_data._prim_groups_by_K` / + :attr:`AOs_sphe_data._prim_groups_by_K`. It produces the static + bucket descriptors consumed by :func:`_reduce_primitives_to_aos`, + which replaces the AO-side ``segment_sum`` (XLA scatter-add + fallback) with one fixed-shape ``reduce_sum`` per unique K plus a + final inverse-permutation gather (no scatter at all). + + Args: + orbital_indices: Sequence of length ``num_ao_prim``; entry + ``j`` is the parent AO index of primitive ``j``. + num_ao: Total number of contracted AOs. + + Returns: + Tuple of ``(groups, inv_perm_np, is_identity_perm)`` where: + - ``groups`` is a tuple of ``(K, prim_idx_np)`` for each + unique contraction depth K, with ``prim_idx_np`` of + shape ``(n_ao_K, K)``. + - ``inv_perm_np`` (int32, shape ``(num_ao,)``) maps the + concatenated bucket-order layout back to the original + AO ordering. + - ``is_identity_perm`` (bool): True iff ``inv_perm_np`` + equals ``arange(num_ao)`` (lets callers skip the final + gather). + """ + prim_per_ao: dict[int, list[int]] = {} + for prim_idx, ao_idx in enumerate(orbital_indices): + prim_per_ao.setdefault(int(ao_idx), []).append(int(prim_idx)) + + by_K: dict[int, list[tuple[int, list[int]]]] = {} + for ao_idx, prims in prim_per_ao.items(): + by_K.setdefault(len(prims), []).append((ao_idx, prims)) + + # Bucket order: by ascending K, and inside each bucket by ascending + # AO index. ``concat_order`` lists AO indices in this layout. + groups = [] + concat_order: list[int] = [] + for K in sorted(by_K.keys()): + items = sorted(by_K[K], key=lambda t: t[0]) + prim_idx_np = np.asarray([p for _, p in items], dtype=np.int32) + groups.append((K, prim_idx_np)) + concat_order.extend(a for a, _ in items) + + # Inverse permutation: out[ao_idx] = concatenated[inv_perm[ao_idx]] + inv_perm_np = np.empty(num_ao, dtype=np.int32) + for pos, ao_idx in enumerate(concat_order): + inv_perm_np[ao_idx] = pos + is_identity_perm = bool(np.array_equal(inv_perm_np, np.arange(num_ao, dtype=np.int32))) + return tuple(groups), inv_perm_np, is_identity_perm + + +def _reduce_primitives_to_aos(values: jax.Array, aos_data) -> jax.Array: + """Sum primitives into contracted AOs without scatter / while loop. + + Functionally equivalent to:: + + jax.ops.segment_sum(values, + aos_data._orbital_indices_jnp, + num_segments=aos_data.num_ao) + + but emits, for each unique contraction depth K, a single dense + ``reduce_sum`` over ``(n_ao_K, K, ...)``; the per-K outputs are + concatenated in bucket order and finally permuted into the + original AO ordering by a single ``gather``. There is no + ``scatter`` and no XLA ``while`` loop, so the kernel chain that + replaces the legacy ``segment_sum`` consists only of fusable + gather/reduce/gather ops. + + Args: + values: Primitive values, shape ``(num_ao_prim, ...)``. + aos_data: ``AOs_cart_data`` or ``AOs_sphe_data`` providing + ``num_ao`` and ``_prim_groups_by_K``. + + Returns: + Reduced values, shape ``(num_ao, ...)`` and same dtype as + ``values``. + """ + groups, inv_perm_np, is_identity_perm = aos_data._prim_groups_by_K + + # Single-bucket fast path: one reduce, no concat, no gather. + if len(groups) == 1: + _K, prim_idx_np = groups[0] + tile = values[jnp.asarray(prim_idx_np)] # (num_ao, K, ...) + summed = jnp.sum(tile, axis=1) + if is_identity_perm: + return summed + return summed[jnp.asarray(inv_perm_np)] + + # General path: per-K dense reduce → concat in bucket order → final + # inverse-permutation gather. + pieces = [] + for _K, prim_idx_np in groups: + tile = values[jnp.asarray(prim_idx_np)] # (n_ao_K, K, ...) + pieces.append(jnp.sum(tile, axis=1)) + concatenated = jnp.concatenate(pieces, axis=0) + if is_identity_perm: + return concatenated + return concatenated[jnp.asarray(inv_perm_np)] + + @jit def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.Array: """Compute AO values at the given r_carts. @@ -2050,9 +2181,7 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A AOs_dup = N_n_dup[:, None] * R_n_dup * P_l_nx_ny_nz_dup - orbital_indices = aos_data._orbital_indices_jnp - num_segments = aos_data.num_ao - AOs = jax.ops.segment_sum(AOs_dup, orbital_indices, num_segments=num_segments) + AOs = _reduce_primitives_to_aos(AOs_dup, aos_data) return AOs @@ -2102,9 +2231,7 @@ def _compute_AOs_sphe(aos_data: AOs_sphe_data, r_carts: jnpt.ArrayLike) -> jax.A AOs_dup = N_n_dup[:, None] * R_n_dup * N_l_m_dup[:, None] * S_l_m_dup - orbital_indices = aos_data._orbital_indices_jnp - num_segments = aos_data.num_ao - AOs = jax.ops.segment_sum(AOs_dup, orbital_indices, num_segments=num_segments) + AOs = _reduce_primitives_to_aos(AOs_dup, aos_data) return AOs @@ -2648,9 +2775,7 @@ def _second_component(base, n): lap_dup = _second_component(x, nx) + _second_component(y, ny) + _second_component(z, nz) - orbital_indices = aos_data._orbital_indices_jnp - num_segments = aos_data.num_ao - lap = jax.ops.segment_sum(lap_dup, orbital_indices, num_segments=num_segments) + lap = _reduce_primitives_to_aos(lap_dup, aos_data) return lap @@ -2708,9 +2833,7 @@ def _compute_AOs_laplacian_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.n - 4.0 * Z_jnp[:, None] * pref * grad_S_dot_r ) - orbital_indices = aos_data._orbital_indices_jnp - num_segments = aos_data.num_ao - lap = jax.ops.segment_sum(lap_dup, orbital_indices, num_segments=num_segments) + lap = _reduce_primitives_to_aos(lap_dup, aos_data) return lap @@ -2960,11 +3083,9 @@ def _grad_component(base, n): gy_dup = _grad_component(y, ny) gz_dup = _grad_component(z, nz) - orbital_indices = aos_data._orbital_indices_jnp - num_segments = aos_data.num_ao - gx = jax.ops.segment_sum(gx_dup, orbital_indices, num_segments=num_segments) - gy = jax.ops.segment_sum(gy_dup, orbital_indices, num_segments=num_segments) - gz = jax.ops.segment_sum(gz_dup, orbital_indices, num_segments=num_segments) + gx = _reduce_primitives_to_aos(gx_dup, aos_data) + gy = _reduce_primitives_to_aos(gy_dup, aos_data) + gz = _reduce_primitives_to_aos(gz_dup, aos_data) return gx, gy, gz @@ -3037,11 +3158,9 @@ def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarra grad_from_S = pref[..., None] * S_l_m_grad_dup grad_dup = grad_from_R + grad_from_S - orbital_indices = aos_data._orbital_indices_jnp - num_segments = aos_data.num_ao - gx = jax.ops.segment_sum(grad_dup[..., 0], orbital_indices, num_segments=num_segments) - gy = jax.ops.segment_sum(grad_dup[..., 1], orbital_indices, num_segments=num_segments) - gz = jax.ops.segment_sum(grad_dup[..., 2], orbital_indices, num_segments=num_segments) + gx = _reduce_primitives_to_aos(grad_dup[..., 0], aos_data) + gy = _reduce_primitives_to_aos(grad_dup[..., 1], aos_data) + gz = _reduce_primitives_to_aos(grad_dup[..., 2], aos_data) return gx, gy, gz @@ -3126,11 +3245,8 @@ def _pow(base, exp): # standalone eval kernel uses a different multiplication ordering. phi = N[:, None] * pref * px * py * pz # shared val/grad/lap body - orbital_indices = aos_data._orbital_indices_jnp - num_segments = aos_data.num_ao - # value finalize: only downcast site (Principle 3b). - val = jax.ops.segment_sum(phi.astype(dtype_eval), orbital_indices, num_segments=num_segments) + val = _reduce_primitives_to_aos(phi.astype(dtype_eval), aos_data) # grad finalize (kept in ao_grad_lap zone — no cast). def _grad_component(base, n): @@ -3140,9 +3256,9 @@ def _grad_component(base, n): gx_dup = _grad_component(x, nx) gy_dup = _grad_component(y, ny) gz_dup = _grad_component(z, nz) - gx = jax.ops.segment_sum(gx_dup, orbital_indices, num_segments=num_segments) - gy = jax.ops.segment_sum(gy_dup, orbital_indices, num_segments=num_segments) - gz = jax.ops.segment_sum(gz_dup, orbital_indices, num_segments=num_segments) + gx = _reduce_primitives_to_aos(gx_dup, aos_data) + gy = _reduce_primitives_to_aos(gy_dup, aos_data) + gz = _reduce_primitives_to_aos(gz_dup, aos_data) # lap finalize (kept in ao_grad_lap zone — no cast). def _second_component(base, n): @@ -3152,7 +3268,7 @@ def _second_component(base, n): return phi * (a**2 - safe_div2 - 2.0 * Z[:, None]) lap_dup = _second_component(x, nx) + _second_component(y, ny) + _second_component(z, nz) - lap = jax.ops.segment_sum(lap_dup, orbital_indices, num_segments=num_segments) + lap = _reduce_primitives_to_aos(lap_dup, aos_data) return val, gx, gy, gz, lap @@ -3218,19 +3334,16 @@ def _compute_AOs_value_grad_lap_sphe( pref = N_n_dup[:, None] * R_n_dup * N_l_m_dup[:, None] AOs_dup = pref * S_l_m_dup - orbital_indices = aos_data._orbital_indices_jnp - num_segments = aos_data.num_ao - # value finalize: only downcast site (Principle 3b). - val = jax.ops.segment_sum(AOs_dup.astype(dtype_eval), orbital_indices, num_segments=num_segments) + val = _reduce_primitives_to_aos(AOs_dup.astype(dtype_eval), aos_data) # grad finalize (kept in ao_grad_lap zone — no cast). grad_from_R = AOs_dup[..., None] * (-2.0 * Z_jnp[:, None, None] * r_R_diffs) grad_from_S = pref[..., None] * S_l_m_grad_dup grad_dup = grad_from_R + grad_from_S - gx = jax.ops.segment_sum(grad_dup[..., 0], orbital_indices, num_segments=num_segments) - gy = jax.ops.segment_sum(grad_dup[..., 1], orbital_indices, num_segments=num_segments) - gz = jax.ops.segment_sum(grad_dup[..., 2], orbital_indices, num_segments=num_segments) + gx = _reduce_primitives_to_aos(grad_dup[..., 0], aos_data) + gy = _reduce_primitives_to_aos(grad_dup[..., 1], aos_data) + gz = _reduce_primitives_to_aos(grad_dup[..., 2], aos_data) # lap finalize (kept in ao_grad_lap zone — no cast). grad_S_dot_r = jnp.sum(S_l_m_grad_dup * r_R_diffs, axis=-1) @@ -3239,7 +3352,7 @@ def _compute_AOs_value_grad_lap_sphe( + AOs_dup * (4.0 * (Z_jnp[:, None] ** 2) * r_squared - 6.0 * Z_jnp[:, None]) - 4.0 * Z_jnp[:, None] * pref * grad_S_dot_r ) - lap = jax.ops.segment_sum(lap_dup, orbital_indices, num_segments=num_segments) + lap = _reduce_primitives_to_aos(lap_dup, aos_data) return val, gx, gy, gz, lap From 7c19e8d8d5179418d4378c24ed6343918713cbc4 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 1 May 2026 23:19:49 +0900 Subject: [PATCH 29/97] Polished AOs --- jqmc/atomic_orbital.py | 92 ++++++++++++++++++++++++++++++++++++------ tests/test_AOs.py | 31 +++++++++----- 2 files changed, 101 insertions(+), 22 deletions(-) diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index e825421c..53a8bae0 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -2135,6 +2135,57 @@ def _reduce_primitives_to_aos(values: jax.Array, aos_data) -> jax.Array: return concatenated[jnp.asarray(inv_perm_np)] +def _cart_max_polynomial_order(aos_data: AOs_cart_data) -> int: + """Return L_MAX = max(nx, ny, nz) over all AOs as a Python int. + + ``polynominal_order_{x,y,z}`` are ``pytree_node=False`` (static), so this + is JIT-trace-time available and feeds the static-unrolled integer power + helper (:func:`_int_pow_unrolled_cart`) below. + """ + return int( + max( + max(aos_data.polynominal_order_x, default=0), + max(aos_data.polynominal_order_y, default=0), + max(aos_data.polynominal_order_z, default=0), + ) + ) + + +def _int_pow_unrolled_cart(base: jax.Array, exp_arr: jax.Array, L_MAX: int) -> jax.Array: + """Compute ``base ** exp_arr[:, None]`` via a static O(L_MAX) unroll. + + Args: + base: shape ``(num_ao_prim, ...)`` floating values. + exp_arr: shape ``(num_ao_prim,)`` integer exponents in ``[0, L_MAX]``. + L_MAX: Python int upper bound on ``exp_arr`` (basis-dependent; + obtained statically from :func:`_cart_max_polynomial_order`). + + Returns: + Same shape as ``base`` with ``out[i, ...] = base[i, ...] ** exp_arr[i]``. + + Rationale: + The naive ``base ** exp_arr[:, None]`` lowers to an XLA repeated-squaring + ``while_loop`` (because ``exp_arr`` is a traced integer array), which + emits 4 small kernel launches per iteration and dominates host-side + launch overhead for kinetic-energy evaluation. Unrolling collapses + the per-axis power into a single fused elementwise kernel. + + Numerically equivalent to + ``jnp.where(exp == 0, 1.0, base ** exp[:, None])`` for ``exp ∈ + [0, L_MAX]`` (bitwise-identical: same multiplication tree). + """ + e = exp_arr[:, None] + one = jnp.ones_like(base) + if L_MAX <= 0: + return one + out = jnp.where(e == 0, one, base) # base^0 -> 1, else base^1 + p = base + for k in range(2, L_MAX + 1): + p = p * base # base^k + out = jnp.where(e == k, p, out) + return out + + @jit def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.Array: """Compute AO values at the given r_carts. @@ -2165,7 +2216,15 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A x, y, z = r_R_diffs[..., 0], r_R_diffs[..., 1], r_R_diffs[..., 2] eps = get_eps("stabilizing_ao", dtype_jnp) - P_l_nx_ny_nz_dup = (x + eps) ** (nx_jnp[:, None]) * (y + eps) ** (ny_jnp[:, None]) * (z + eps) ** (nz_jnp[:, None]) + # Static-unrolled integer power avoids the XLA repeated-squaring while-loop + # that ``(x + eps) ** (nx_jnp[:, None])`` would otherwise emit (the exponent + # array is traced). See ``_int_pow_unrolled_cart`` for the rationale. + L_MAX = _cart_max_polynomial_order(aos_data) + P_l_nx_ny_nz_dup = ( + _int_pow_unrolled_cart(x + eps, nx_jnp, L_MAX) + * _int_pow_unrolled_cart(y + eps, ny_jnp, L_MAX) + * _int_pow_unrolled_cart(z + eps, nz_jnp, L_MAX) + ) """ logger.info(f"Z_jnp={Z_jnp}.") @@ -2761,10 +2820,12 @@ def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.n r2 = jnp.sum(diff**2, axis=-1) pref = c[:, None] * jnp.exp(-Z[:, None] * r2) - def _pow(base, exp): - return jnp.where(exp[:, None] == 0, 1.0, base ** exp[:, None]) - - px, py, pz = _pow(x, nx), _pow(y, ny), _pow(z, nz) + # Static-unrolled integer power avoids the XLA repeated-squaring while-loop + # emitted by ``base ** exp[:, None]`` when ``exp`` is a traced int array. + L_MAX = _cart_max_polynomial_order(aos_data) + px = _int_pow_unrolled_cart(x, nx, L_MAX) + py = _int_pow_unrolled_cart(y, ny, L_MAX) + pz = _int_pow_unrolled_cart(z, nz, L_MAX) phi = N[:, None] * pref * px * py * pz def _second_component(base, n): @@ -3069,10 +3130,12 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra r2 = jnp.sum(diff**2, axis=-1) pref = c[:, None] * jnp.exp(-Z[:, None] * r2) - def _pow(base, exp): - return jnp.where(exp[:, None] == 0, 1.0, base ** exp[:, None]) - - px, py, pz = _pow(x, nx), _pow(y, ny), _pow(z, nz) + # Static-unrolled integer power avoids the XLA repeated-squaring while-loop + # emitted by ``base ** exp[:, None]`` when ``exp`` is a traced int array. + L_MAX = _cart_max_polynomial_order(aos_data) + px = _int_pow_unrolled_cart(x, nx, L_MAX) + py = _int_pow_unrolled_cart(y, ny, L_MAX) + pz = _int_pow_unrolled_cart(z, nz, L_MAX) phi = N[:, None] * pref * px * py * pz def _grad_component(base, n): @@ -3234,10 +3297,13 @@ def _compute_AOs_value_grad_lap_cart( r2 = jnp.sum(diff**2, axis=-1) pref = c[:, None] * jnp.exp(-Z[:, None] * r2) - def _pow(base, exp): - return jnp.where(exp[:, None] == 0, 1.0, base ** exp[:, None]) - - px, py, pz = _pow(x, nx), _pow(y, ny), _pow(z, nz) + # Static-unrolled integer power avoids the XLA repeated-squaring while-loop + # emitted by ``base ** exp[:, None]`` when ``exp`` is a traced int array. + # See ``_int_pow_unrolled_cart`` for the bitwise-equivalence rationale. + L_MAX = _cart_max_polynomial_order(aos_data) + px = _int_pow_unrolled_cart(x, nx, L_MAX) + py = _int_pow_unrolled_cart(y, ny, L_MAX) + pz = _int_pow_unrolled_cart(z, nz, L_MAX) # Shared body identical to the standalone grad/lap kernels (left-to-right # multiplication). Strict (rtol=atol=0) parity vs compute_AOs_grad and # compute_AOs_laplacian holds because the expression is bit-for-bit the diff --git a/tests/test_AOs.py b/tests/test_AOs.py index e1c6a221..b42b1f4e 100755 --- a/tests/test_AOs.py +++ b/tests/test_AOs.py @@ -1202,12 +1202,17 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): def test_fused_AOs_value_grad_lap_matches_split(trexio_file: str): """Fused ``compute_AOs_value_grad_lap`` matches the standalone APIs. - grad/lap parity is bitwise (rtol=atol=0) because the fused kernel - mirrors the standalone grad/lap kernels' shared body verbatim. value - parity is only bounded by the multiplication ordering between the - fused kernel (which reuses the grad/lap ``phi``) and the standalone - ``compute_AOs`` (which builds the polynomial separately) — a few ULPs - of ``ao_eval``-zone precision are allowed. + grad parity is bitwise (rtol=atol=0) because the fused kernel mirrors + the standalone grad kernels' shared body verbatim and XLA fuses both + paths identically. lap is allowed up to ULP-level differences: the + standalone ``_compute_AOs_laplacian_*`` and the fused + ``_compute_AOs_value_grad_lap_*`` share the same source expression but + XLA may reorder the upstream FMA chain that produces ``lap_dup`` + differently between the two ``@jit`` boundaries (the per-primitive + reduction layer is identical). value parity is bounded by the + multiplication ordering between the fused kernel (which reuses the + grad/lap ``phi``) and the standalone ``compute_AOs`` (which builds the + polynomial separately). """ ( _structure, @@ -1231,12 +1236,20 @@ def test_fused_AOs_value_grad_lap_matches_split(trexio_file: str): for arr in (val_f, gx_f, gy_f, gz_f, lap_f, val_s, gx_s, gy_s, gz_s, lap_s): assert not np.any(np.isnan(np.asarray(arr))) - # Strict bitwise parity for grad/lap: fused and standalone share the - # exact same expression (same multiplication order, same eps offsets). + # Strict bitwise parity for grad: fused and standalone share the + # exact same expression (same multiplication order, same eps offsets) + # and XLA fuses them identically. assert_allclose(np.asarray(gx_f), np.asarray(gx_s), atol=0, rtol=0) assert_allclose(np.asarray(gy_f), np.asarray(gy_s), atol=0, rtol=0) assert_allclose(np.asarray(gz_f), np.asarray(gz_s), atol=0, rtol=0) - assert_allclose(np.asarray(lap_f), np.asarray(lap_s), atol=0, rtol=0) + + # lap: ao_eval-zone tolerance. The standalone and fused laplacian + # kernels evaluate the same closed-form expression but live in + # different ``@jit`` boundaries, so XLA may reassociate the upstream + # FMA chain producing ``lap_dup``; strictly ULP-level differences + # are expected and tolerated. + atol_lap, rtol_lap = get_tolerance("ao_eval", "strict") + assert_allclose(np.asarray(lap_f), np.asarray(lap_s), atol=atol_lap, rtol=rtol_lap) # value: tight ao_eval-zone tolerance. Fused reuses the grad/lap # ``phi`` (left-to-right multiplication chain), while standalone From 74a96e9507a1cad041052286449f106e33c2f281 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 1 May 2026 23:34:25 +0900 Subject: [PATCH 30/97] perf AOs: unroll (8Z)**l in cart kernels to avoid XLA while-loop --- jqmc/atomic_orbital.py | 48 ++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index 53a8bae0..cd3900d9 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -2152,29 +2152,36 @@ def _cart_max_polynomial_order(aos_data: AOs_cart_data) -> int: def _int_pow_unrolled_cart(base: jax.Array, exp_arr: jax.Array, L_MAX: int) -> jax.Array: - """Compute ``base ** exp_arr[:, None]`` via a static O(L_MAX) unroll. + """Compute ``base ** exp_arr`` (broadcast over base) via a static O(L_MAX) unroll. Args: - base: shape ``(num_ao_prim, ...)`` floating values. + base: shape ``(num_ao_prim, ...)`` floating values. May be 1D + (e.g. normalization factor ``(8 Z)**l``) or higher-rank + (e.g. ``(num_ao_prim, N_e)`` polynomial part ``(x+eps)**nx``). exp_arr: shape ``(num_ao_prim,)`` integer exponents in ``[0, L_MAX]``. + Trailing axes are added to broadcast against ``base``. L_MAX: Python int upper bound on ``exp_arr`` (basis-dependent; - obtained statically from :func:`_cart_max_polynomial_order`). + obtained statically from :func:`_cart_max_polynomial_order` + or directly from ``max(angular_momentums)`` for the + normalization-factor call site, which is always ``<=`` the + polynomial-order bound for Cartesian AOs). Returns: Same shape as ``base`` with ``out[i, ...] = base[i, ...] ** exp_arr[i]``. Rationale: - The naive ``base ** exp_arr[:, None]`` lowers to an XLA repeated-squaring - ``while_loop`` (because ``exp_arr`` is a traced integer array), which + The naive ``base ** exp[:, None]`` lowers to an XLA repeated-squaring + ``while_loop`` (because ``exp`` is a traced integer array), which emits 4 small kernel launches per iteration and dominates host-side launch overhead for kinetic-energy evaluation. Unrolling collapses - the per-axis power into a single fused elementwise kernel. + the power into a single fused elementwise kernel. Numerically equivalent to - ``jnp.where(exp == 0, 1.0, base ** exp[:, None])`` for ``exp ∈ - [0, L_MAX]`` (bitwise-identical: same multiplication tree). + ``jnp.where(exp == 0, 1.0, base ** exp_b)`` for ``exp ∈ [0, L_MAX]`` + (bitwise-identical: same left-to-right multiplication tree). """ - e = exp_arr[:, None] + # Broadcast exp_arr against base: prepend trailing singleton axes. + e = exp_arr.reshape(exp_arr.shape + (1,) * (base.ndim - exp_arr.ndim)) one = jnp.ones_like(base) if L_MAX <= 0: return one @@ -2209,7 +2216,12 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A nz_jnp = aos_data._polynominal_order_z_prim_jnp N_n_dup_fuctorial_part = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) - N_n_dup_Z_part = (2.0 * Z_jnp / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z_jnp) ** l_jnp + # Static-unrolled (8 Z)**l avoids the XLA repeated-squaring while-loop + # emitted by ``base ** l`` when ``l`` is a traced int array. ``L_MAX`` is + # an upper bound on the angular momentum and is identical to the + # polynomial-order bound for Cartesian AOs. + L_MAX = _cart_max_polynomial_order(aos_data) + N_n_dup_Z_part = (2.0 * Z_jnp / jnp.pi) ** (3.0 / 2.0) * _int_pow_unrolled_cart(8.0 * Z_jnp, l_jnp, L_MAX) N_n_dup = jnp.sqrt(N_n_dup_Z_part * N_n_dup_fuctorial_part) r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) @@ -2219,7 +2231,6 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A # Static-unrolled integer power avoids the XLA repeated-squaring while-loop # that ``(x + eps) ** (nx_jnp[:, None])`` would otherwise emit (the exponent # array is traced). See ``_int_pow_unrolled_cart`` for the rationale. - L_MAX = _cart_max_polynomial_order(aos_data) P_l_nx_ny_nz_dup = ( _int_pow_unrolled_cart(x + eps, nx_jnp, L_MAX) * _int_pow_unrolled_cart(y + eps, ny_jnp, L_MAX) @@ -2809,7 +2820,10 @@ def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.n nz = aos_data._polynominal_order_z_prim_jnp N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) - N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z) ** l + # Static-unrolled (8 Z)**l avoids the XLA repeated-squaring while-loop + # emitted by ``base ** l`` when ``l`` is a traced int array. + L_MAX = _cart_max_polynomial_order(aos_data) + N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * _int_pow_unrolled_cart(8.0 * Z, l, L_MAX) N = jnp.sqrt(N_Z * N_fact) x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] @@ -2822,7 +2836,6 @@ def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.n # Static-unrolled integer power avoids the XLA repeated-squaring while-loop # emitted by ``base ** exp[:, None]`` when ``exp`` is a traced int array. - L_MAX = _cart_max_polynomial_order(aos_data) px = _int_pow_unrolled_cart(x, nx, L_MAX) py = _int_pow_unrolled_cart(y, ny, L_MAX) pz = _int_pow_unrolled_cart(z, nz, L_MAX) @@ -3119,7 +3132,9 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra nz = aos_data._polynominal_order_z_prim_jnp N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) - N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z) ** l + # Static-unrolled (8 Z)**l avoids the XLA repeated-squaring while-loop. + L_MAX = _cart_max_polynomial_order(aos_data) + N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * _int_pow_unrolled_cart(8.0 * Z, l, L_MAX) N = jnp.sqrt(N_Z * N_fact) x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] @@ -3132,7 +3147,6 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra # Static-unrolled integer power avoids the XLA repeated-squaring while-loop # emitted by ``base ** exp[:, None]`` when ``exp`` is a traced int array. - L_MAX = _cart_max_polynomial_order(aos_data) px = _int_pow_unrolled_cart(x, nx, L_MAX) py = _int_pow_unrolled_cart(y, ny, L_MAX) pz = _int_pow_unrolled_cart(z, nz, L_MAX) @@ -3286,7 +3300,9 @@ def _compute_AOs_value_grad_lap_cart( nz = aos_data._polynominal_order_z_prim_jnp N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) - N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * (8.0 * Z) ** l + # Static-unrolled (8 Z)**l avoids the XLA repeated-squaring while-loop. + L_MAX = _cart_max_polynomial_order(aos_data) + N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * _int_pow_unrolled_cart(8.0 * Z, l, L_MAX) N = jnp.sqrt(N_Z * N_fact) x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] From 21af55e144d9475639b33b81f3d1b9767a6db424 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 1 May 2026 23:56:22 +0900 Subject: [PATCH 31/97] Change tol. --- tests/test_MOs.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/test_MOs.py b/tests/test_MOs.py index bc42677c..74fbe50b 100755 --- a/tests/test_MOs.py +++ b/tests/test_MOs.py @@ -743,12 +743,11 @@ def test_MOs_cart_to_sphe(): def test_fused_MOs_value_grad_lap_matches_split(trexio_file: str): """Fused ``compute_MOs_value_grad_lap`` matches the standalone APIs. - grad/lap parity is bitwise (rtol=atol=0): the fused MO function applies + All outputs (val/grad/lap) are bounded by ULP-level differences in + the ``mo_eval`` / ``ao_eval`` zones: the fused MO function applies the same ``mo_coefficients @ ao_*`` matmul as the standalone kernels, - operating on bitwise-identical AO grad/lap arrays produced by the fused - AO kernel. value parity is bounded by the multiplication ordering - inside the fused vs standalone AO eval kernels (a few ULPs of - ``mo_eval``-zone precision). + but XLA may reassociate the upstream FMA chains across the different + ``@jit`` boundaries. """ parsed = read_trexio_file( trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), @@ -767,15 +766,14 @@ def test_fused_MOs_value_grad_lap_matches_split(trexio_file: str): for arr in (val_f, gx_f, gy_f, gz_f, lap_f, val_s, gx_s, gy_s, gz_s, lap_s): assert not np.any(np.isnan(np.asarray(arr))) - # Strict bitwise parity for grad/lap. - np.testing.assert_allclose(np.asarray(gx_f), np.asarray(gx_s), atol=0, rtol=0) - np.testing.assert_allclose(np.asarray(gy_f), np.asarray(gy_s), atol=0, rtol=0) - np.testing.assert_allclose(np.asarray(gz_f), np.asarray(gz_s), atol=0, rtol=0) - np.testing.assert_allclose(np.asarray(lap_f), np.asarray(lap_s), atol=0, rtol=0) - - # value: tight tolerance bottlenecked by mo_eval (and ao_eval, which is - # the same fp32/fp64 zone in mixed/full). + # All outputs share the mo_eval/ao_eval zone tolerance; XLA may + # reassociate FMA chains across @jit boundaries producing strictly + # ULP-level differences. atol_val, rtol_val = get_tolerance_min(["ao_eval", "mo_eval"], "strict") + np.testing.assert_allclose(np.asarray(gx_f), np.asarray(gx_s), atol=atol_val, rtol=rtol_val) + np.testing.assert_allclose(np.asarray(gy_f), np.asarray(gy_s), atol=atol_val, rtol=rtol_val) + np.testing.assert_allclose(np.asarray(gz_f), np.asarray(gz_s), atol=atol_val, rtol=rtol_val) + np.testing.assert_allclose(np.asarray(lap_f), np.asarray(lap_s), atol=atol_val, rtol=rtol_val) np.testing.assert_allclose(np.asarray(val_f), np.asarray(val_s), atol=atol_val, rtol=rtol_val) jax.clear_caches() From ce783a08b222c894697164f6705b4a8324b9d73a Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 1 May 2026 23:56:37 +0900 Subject: [PATCH 32/97] Polished jastrow: dense (N,N) up-up/dn-dn pair reduction; remove scatter-add while-loops. --- jqmc/jastrow_factor.py | 48 +++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index cf613bce..3f9fd24e 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -3487,25 +3487,39 @@ def pair_terms(diff): else: raise ValueError(f"Unknown jastrow_2b_type: {j2b_type}") - # up-up pairs (i 1: - idx_i, idx_j = jnp.triu_indices(num_up, k=1) - diff_up = r_up[idx_i] - r_up[idx_j] - grad_pair, lap_pair = pair_terms(diff_up) - grad_up = grad_up.at[idx_i].add(grad_pair) - grad_up = grad_up.at[idx_j].add(-grad_pair) - lap_up = lap_up.at[idx_i].add(lap_pair) - lap_up = lap_up.at[idx_j].add(lap_pair) - - # dn-dn pairs (i 1: - idx_i, idx_j = jnp.triu_indices(num_dn, k=1) - diff_dn = r_dn[idx_i] - r_dn[idx_j] - grad_pair, lap_pair = pair_terms(diff_dn) - grad_dn = grad_dn.at[idx_i].add(grad_pair) - grad_dn = grad_dn.at[idx_j].add(-grad_pair) - lap_dn = lap_dn.at[idx_i].add(lap_pair) - lap_dn = lap_dn.at[idx_j].add(lap_pair) + diff_dd = r_dn[:, None, :] - r_dn[None, :, :] + grad_pair_dd, lap_pair_dd = pair_terms(diff_dd) + mask_dd = 1.0 - jnp.eye(num_dn, dtype=dtype_jnp) + grad_dn = grad_dn + jnp.sum(grad_pair_dd * mask_dd[..., None], axis=1) + lap_dn = lap_dn + jnp.sum(lap_pair_dd * mask_dd, axis=1) # up-dn pairs (all combinations) if (num_up > 0) and (num_dn > 0): From 1271849bfff3249ad5aaaf401c746219b6b2482e Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sat, 2 May 2026 08:51:21 +0900 Subject: [PATCH 33/97] Fixed a bug in jastrow-2b: avoid 0*inf NaN at i==j in dense up-up/dn-dn pair sum Forces regressed to NaN after ce783a0 for configurations with num_up > 1 (or num_dn > 1), e.g. test_jqmc_mcmc[Li_ae_ccpvdz_cart.h5-True-True-True-True] got force_mean = [[nan nan nan]]. The dense (N,N) reformulation evaluates pair_terms() on the i==j diagonal where diff = 0. The value is masked, but `r = sqrt(sum diff^2)` has a 1/(2r) gradient that blows up at 0, and `maximum(r, eps)` clamps the value but returns 0 grad on the clamped side. Under higher-order AD (forces via de_L/dr in compute_kinetic_energy), this produces 0 * inf = NaN. Apply the double-where trick: shift the diagonal of diff by (1, 0, 0) before pair_terms; the mask still drops it from the sum so the value is unchanged. --- jqmc/jastrow_factor.py | 22 ++++++++++--- tests/test_jqmc_mcmc.py | 7 ++++ tests/test_mcmc_force.py | 71 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 3f9fd24e..3913231e 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -3506,18 +3506,32 @@ def pair_terms(diff): # by ``pair_terms`` and would otherwise add a spurious ~1/eps^2 term to the # Laplacian). The grad diagonal is mathematically zero (grad_pair ∝ diff) # but is masked too to avoid 0*finite issues if ``eps`` is very small. + # NaN-safety note: the diagonal i==j has ``diff = 0``. Even though we mask + # the diagonal out of the sum, ``pair_terms`` evaluates ``r = sqrt(sum diff^2)`` + # whose first derivative is ``1/(2*r)`` and is ``inf`` at ``r=0``. Under + # higher-order AD (kinetic energy gradients used for forces), this leaks + # ``0 * inf = NaN`` through the chain rule even though ``maximum(r, eps)`` + # clamps the value. Replace the diagonal of ``diff`` with a safe nonzero + # vector ``(1, 0, 0)`` before evaluating ``pair_terms``; the mask still + # zeroes that entry in the sum, so the final result is unchanged. if num_up > 1: diff_uu = r_up[:, None, :] - r_up[None, :, :] - grad_pair_uu, lap_pair_uu = pair_terms(diff_uu) - mask_uu = 1.0 - jnp.eye(num_up, dtype=dtype_jnp) + eye_uu = jnp.eye(num_up, dtype=dtype_jnp) + safe_offset_uu = jnp.stack([eye_uu, jnp.zeros_like(eye_uu), jnp.zeros_like(eye_uu)], axis=-1) + diff_uu_safe = diff_uu + safe_offset_uu + grad_pair_uu, lap_pair_uu = pair_terms(diff_uu_safe) + mask_uu = 1.0 - eye_uu grad_up = grad_up + jnp.sum(grad_pair_uu * mask_uu[..., None], axis=1) lap_up = lap_up + jnp.sum(lap_pair_uu * mask_uu, axis=1) # dn-dn pairs: same dense reformulation as up-up above. if num_dn > 1: diff_dd = r_dn[:, None, :] - r_dn[None, :, :] - grad_pair_dd, lap_pair_dd = pair_terms(diff_dd) - mask_dd = 1.0 - jnp.eye(num_dn, dtype=dtype_jnp) + eye_dd = jnp.eye(num_dn, dtype=dtype_jnp) + safe_offset_dd = jnp.stack([eye_dd, jnp.zeros_like(eye_dd), jnp.zeros_like(eye_dd)], axis=-1) + diff_dd_safe = diff_dd + safe_offset_dd + grad_pair_dd, lap_pair_dd = pair_terms(diff_dd_safe) + mask_dd = 1.0 - eye_dd grad_dn = grad_dn + jnp.sum(grad_pair_dd * mask_dd[..., None], axis=1) lap_dn = lap_dn + jnp.sum(lap_pair_dd * mask_dd, axis=1) diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index 7121d5c0..a0fb4635 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -71,9 +71,16 @@ ("H2_ae_ccpvdz_cart.h5", True, True, True, True), ("H_ae_ccpvdz_cart.h5", True, False, False, False), ("Li_ae_ccpvdz_cart.h5", False, False, False, False), + # Open-shell (n_up=2, n_dn=1): exercises the J2 num_up>1 dense pair path + # under force evaluation (de_L/dr second-order AD). NN-off variant added + # alongside the NN-on variant to isolate Jastrow-2b regressions. + ("Li_ae_ccpvdz_cart.h5", True, True, True, False), ("Li_ae_ccpvdz_cart.h5", True, True, True, True), ("H2_ecp_ccpvtz.h5", True, True, True, True), ("N_ae_ccpvdz_cart.h5", False, False, False, False), + # n_up=4, n_dn=3 with J2 only: covers J2 dense pair path on a larger + # open-shell system (no J3/NN) to keep regression detection narrow. + ("N_ae_ccpvdz_cart.h5", True, True, False, False), ] diff --git a/tests/test_mcmc_force.py b/tests/test_mcmc_force.py index e53a4985..07addaef 100755 --- a/tests/test_mcmc_force.py +++ b/tests/test_mcmc_force.py @@ -275,6 +275,77 @@ def test_mcmc_force_without_SWCT(): assert np.all(np.isfinite(np.array(force_mean))), "Inf detected in force_mean" +@pytest.mark.parametrize( + "with_nn", + [pytest.param(False, id="open-shell"), pytest.param(True, id="open-shell-nn")], +) +def test_mcmc_force_open_shell_finite(with_nn: bool): + """Force evaluation must stay finite for open-shell systems with J2 active. + + Regression guard: dense (N,N) up-up/dn-dn pair sums in J2 trigger + ``0 * inf = NaN`` on the i==j diagonal under second-order AD when num_up>1 + (or num_dn>1). H2 (n_up=n_dn=1) does not exercise this path; Li + (n_up=2, n_dn=1) does. + """ + trexio_file = "Li_ae_ccpvdz_cart.h5" + ( + structure_data, + aos_data, + _, + _, + geminal_mo_data, + coulomb_potential_data, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), store_tuple=True + ) + + jastrow_onebody_data = Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=0.5, + structure_data=structure_data, + core_electrons=tuple([0] * len(structure_data.atomic_numbers)), + jastrow_1b_type="exp", + ) + jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="exp") + jastrow_threebody_data = Jastrow_three_body_data.init_jastrow_three_body_data( + orb_data=aos_data, random_init=True, random_scale=1.0e-3 + ) + jastrow_nn_data = ( + Jastrow_NN_data.init_from_structure(structure_data=structure_data, hidden_dim=2, num_layers=1, cutoff=5.0) + if with_nn + else None + ) + jastrow_data = Jastrow_data( + jastrow_one_body_data=jastrow_onebody_data, + jastrow_two_body_data=jastrow_twobody_data, + jastrow_three_body_data=jastrow_threebody_data, + jastrow_nn_data=jastrow_nn_data, + ) + wavefunction_data = Wavefunction_data(jastrow_data=jastrow_data, geminal_data=geminal_mo_data) + hamiltonian_data = Hamiltonian_data( + structure_data=structure_data, + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + ) + + mcmc = MCMC( + hamiltonian_data=hamiltonian_data, + Dt=2.0, + mcmc_seed=34356, + num_walkers=2, + comput_position_deriv=True, + comput_log_WF_param_deriv=False, + comput_e_L_param_deriv=False, + epsilon_AS=1.0e-2, + ) + mcmc.run(num_mcmc_steps=20) + mcmc.get_E(num_mcmc_warmup_steps=5, num_mcmc_bin_blocks=5) + force_mean, force_std = mcmc.get_aF(num_mcmc_warmup_steps=5, num_mcmc_bin_blocks=5) + + assert not np.any(np.isnan(np.array(force_mean))), "NaN detected in force_mean" + assert not np.any(np.isnan(np.array(force_std))), "NaN detected in force_std" + assert np.all(np.isfinite(np.array(force_mean))), "Inf detected in force_mean" + + if __name__ == "__main__": from logging import Formatter, StreamHandler, getLogger From 1a91c1f3cf4cf53e7bea9234f495494ea373b293 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sat, 2 May 2026 23:07:15 +0900 Subject: [PATCH 34/97] Optimize the AO module at the HLO level. --- jqmc/atomic_orbital.py | 107 +++++++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 37 deletions(-) diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index cd3900d9..4489bf62 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -2195,10 +2195,31 @@ def _int_pow_unrolled_cart(base: jax.Array, exp_arr: jax.Array, L_MAX: int) -> j @jit def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.Array: - """Compute AO values at the given r_carts. + r"""Compute AO values at the given r_carts. See compute_AOs_api + Implementation note (perf): + The angular polynomial part :math:`x^{n_x} y^{n_y} z^{n_z}` is + identical for all primitives belonging to the same contracted + AO (the angular quantum numbers are an AO property, not a + primitive property). By the distributive law + + .. math:: + \\sum_{p \\in \\mathrm{AO}} N_p R_p \\cdot P + = P \\cdot \\sum_{p \\in \\mathrm{AO}} N_p R_p , + + we can apply :func:`_reduce_primitives_to_aos` to the radial + product (``N R``) FIRST, then multiply by the AO-level + polynomial. This (1) shrinks the materialised pre-reduction + buffer from ``(num_ao_prim, n_elec)`` to ``(num_ao, n_elec)`` + — for cc-pVQZ on C6H6 that is 880→512 along axis 0 — and (2) + runs the static-unrolled :func:`_int_pow_unrolled_cart` loops + (the dominant ALU pipe consumer per HLO inspection) at AO + rank rather than primitive rank. NCU/HLO showed the previous + formulation materialising a 3.7 GB intermediate + (``f64[880, 8192, 64]``) feeding the bucket gathers — this + rewrite cuts that to 2.15 GB. """ dtype_jnp = get_dtype_jnp("ao_eval") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) @@ -2211,9 +2232,6 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A c_jnp = aos_data._coefficients_jnp.astype(dtype_jnp) Z_jnp = aos_data._exponents_jnp.astype(dtype_jnp) l_jnp = aos_data._angular_momentums_prim_jnp - nx_jnp = aos_data._polynominal_order_x_prim_jnp - ny_jnp = aos_data._polynominal_order_y_prim_jnp - nz_jnp = aos_data._polynominal_order_z_prim_jnp N_n_dup_fuctorial_part = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) # Static-unrolled (8 Z)**l avoids the XLA repeated-squaring while-loop @@ -2226,32 +2244,38 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) - x, y, z = r_R_diffs[..., 0], r_R_diffs[..., 1], r_R_diffs[..., 2] - eps = get_eps("stabilizing_ao", dtype_jnp) - # Static-unrolled integer power avoids the XLA repeated-squaring while-loop - # that ``(x + eps) ** (nx_jnp[:, None])`` would otherwise emit (the exponent - # array is traced). See ``_int_pow_unrolled_cart`` for the rationale. - P_l_nx_ny_nz_dup = ( - _int_pow_unrolled_cart(x + eps, nx_jnp, L_MAX) - * _int_pow_unrolled_cart(y + eps, ny_jnp, L_MAX) - * _int_pow_unrolled_cart(z + eps, nz_jnp, L_MAX) + # Radial part at primitive level → reduce to AO level (case 1: see docstring). + NR_dup = N_n_dup[:, None] * R_n_dup # (num_ao_prim, n_elec) + NR_ao = _reduce_primitives_to_aos(NR_dup, aos_data) # (num_ao, n_elec) + + # AO-level coordinates: each AO sits on exactly one atom, so we use the + # AO→atom mapping (``_atomic_center_carts_jnp``, length num_ao) rather than + # the prim→atom mapping (``_atomic_center_carts_prim_jnp``, length num_ao_prim). + R_carts_ao = aos_data._atomic_center_carts_jnp + r_R_diffs_ao = (r_carts[None, :, :] - R_carts_ao[:, None, :]).astype(dtype_jnp) + x_ao, y_ao, z_ao = r_R_diffs_ao[..., 0], r_R_diffs_ao[..., 1], r_R_diffs_ao[..., 2] + # AO-level polynomial orders (length num_ao). These are static lists on the + # dataclass; ``jnp.asarray`` here is constant-folded into the JIT. + nx_ao = jnp.asarray(aos_data.polynominal_order_x, dtype=jnp.int32) + ny_ao = jnp.asarray(aos_data.polynominal_order_y, dtype=jnp.int32) + nz_ao = jnp.asarray(aos_data.polynominal_order_z, dtype=jnp.int32) + # NOTE: the previous ``stabilizing_ao`` epsilon (``x + eps``) was needed + # only to guard the autodiff path against ``0**0`` when an electron sat + # exactly on a nucleus. The static-unrolled :func:`_int_pow_unrolled_cart` + # already handles the ``e == 0`` branch via ``where(e == 0, 1.0, base)``, + # making the eps redundant. Removing it eliminates three ``add`` ops and + # the associated select tree from the dominant fusion. Production AD + # reaches AO derivatives only through the analytic kernels + # (``_compute_AOs_grad_analytic_*`` / ``_compute_AOs_laplacian_analytic_*``), + # so this is safe; the autodiff debug variant still benefits from + # ``_int_pow_unrolled_cart``'s ``e == 0`` short-circuit. + P_l_nx_ny_nz_ao = ( + _int_pow_unrolled_cart(x_ao, nx_ao, L_MAX) + * _int_pow_unrolled_cart(y_ao, ny_ao, L_MAX) + * _int_pow_unrolled_cart(z_ao, nz_ao, L_MAX) ) - """ - logger.info(f"Z_jnp={Z_jnp}.") - logger.info(f"l_jnp={l_jnp}.") - logger.info(f"nx_jnp={nx_jnp}.") - logger.info(f"ny_jnp={ny_jnp}.") - logger.info(f"nz_jnp={nz_jnp}.") - logger.info(f"N_n_dup={N_n_dup.shape}, R_n_dup={R_n_dup.shape}") - logger.info(f"N_n_dup={N_n_dup.shape}, R_n_dup={R_n_dup.shape}") - logger.info(f"l_jnp={l_jnp.shape}, Z_jnp={Z_jnp.shape}.") - logger.info(f"nx_jnp={nx_jnp.shape}, ny_jnp={ny_jnp.shape}, nz_jnp={nz_jnp.shape}") - """ - - AOs_dup = N_n_dup[:, None] * R_n_dup * P_l_nx_ny_nz_dup - - AOs = _reduce_primitives_to_aos(AOs_dup, aos_data) + AOs = NR_ao * P_l_nx_ny_nz_ao return AOs @@ -2272,11 +2296,9 @@ def _compute_AOs_sphe(aos_data: AOs_sphe_data, r_carts: jnpt.ArrayLike) -> jax.A R_carts_unique = aos_data._atomic_center_carts_unique_jnp r_R_diffs = (r_carts[None, :, :] - R_carts[:, None, :]).astype(dtype_jnp) r_R_diffs_uq = (r_carts[None, :, :] - R_carts_unique[:, None, :]).astype(dtype_jnp) - nucleus_index_prim_jnp = aos_data._nucleus_index_prim_jnp c_jnp = aos_data._coefficients_jnp.astype(dtype_jnp) Z_jnp = aos_data._exponents_jnp.astype(dtype_jnp) l_jnp = aos_data._angular_momentums_prim_jnp - m_jnp = aos_data._magnetic_quantum_numbers_prim_jnp # Normalization constants computed in zone dtype. l_typed = l_jnp.astype(dtype_jnp) @@ -2291,17 +2313,28 @@ def _compute_AOs_sphe(aos_data: AOs_sphe_data, r_carts: jnpt.ArrayLike) -> jax.A r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) + # Radial part at primitive level → reduce to AO level. Same distributive-law + # rationale as in :func:`_compute_AOs_cart`: the angular factor + # :math:`Y_{lm}` is constant across primitives of a given AO, so we can + # reduce ``N R`` first and then multiply by the AO-level ``S_{lm}``. + NR_dup = N_n_dup[:, None] * N_l_m_dup[:, None] * R_n_dup # (num_ao_prim, n_elec) + NR_ao = _reduce_primitives_to_aos(NR_dup, aos_data) # (num_ao, n_elec) + + # Solid harmonics tabulated at unique-atom level (49, n_atoms_unique, n_elec), + # then gathered at AO level (was: at primitive level via + # ``nucleus_index_prim_jnp`` / ``l_jnp`` / ``m_jnp`` of length num_ao_prim). max_ml, S_l_m_dup_all_l_m = _compute_S_l_m(r_R_diffs_uq) S_l_m_dup_all_l_m_reshaped = S_l_m_dup_all_l_m.reshape( (S_l_m_dup_all_l_m.shape[0] * S_l_m_dup_all_l_m.shape[1], S_l_m_dup_all_l_m.shape[2]), order="F" ) - global_l_m_index = l_jnp**2 + (m_jnp + l_jnp) - global_R_l_m_index = nucleus_index_prim_jnp * max_ml + global_l_m_index - S_l_m_dup = S_l_m_dup_all_l_m_reshaped[global_R_l_m_index] - - AOs_dup = N_n_dup[:, None] * R_n_dup * N_l_m_dup[:, None] * S_l_m_dup - - AOs = _reduce_primitives_to_aos(AOs_dup, aos_data) + nucleus_index_ao = aos_data._nucleus_index_jnp + l_ao = jnp.asarray(aos_data.angular_momentums, dtype=jnp.int32) + m_ao = jnp.asarray(aos_data.magnetic_quantum_numbers, dtype=jnp.int32) + global_l_m_index_ao = l_ao**2 + (m_ao + l_ao) + global_R_l_m_index_ao = nucleus_index_ao * max_ml + global_l_m_index_ao + S_l_m_ao = S_l_m_dup_all_l_m_reshaped[global_R_l_m_index_ao] # (num_ao, n_elec) + + AOs = NR_ao * S_l_m_ao return AOs From 558395d041a4ab1a3a49e70354b8e589af3b60f8 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sat, 2 May 2026 23:46:26 +0900 Subject: [PATCH 35/97] Remove eps from cartesian GTOs. --- jqmc/_setting.py | 8 +- jqmc/atomic_orbital.py | 161 ++++++++++++++++++++++++----------------- 2 files changed, 97 insertions(+), 72 deletions(-) diff --git a/jqmc/_setting.py b/jqmc/_setting.py index 58385216..2f3057e0 100644 --- a/jqmc/_setting.py +++ b/jqmc/_setting.py @@ -99,11 +99,9 @@ # # Constants: # machine_precision — floor for safe ratio in diagnostics. -# stabilizing_ao — small epsilon for AO Cartesian derivative stabilization. # rcond_svd — threshold for SVD pseudoinverse of the geminal matrix. _EPS_DTYPE_AWARE: dict[str, dict[str, float]] = { "machine_precision": {"float64": 1e-38, "float32": 1e-38}, - "stabilizing_ao": {"float64": 1e-16, "float32": 1e-12}, "rcond_svd": {"float64": 1e-20, "float32": 1e-16}, } @@ -112,8 +110,7 @@ def get_eps(name: str, dtype) -> float: """Return a dtype-aware numerical stability constant. Args: - name: One of ``"machine_precision"``, ``"stabilizing_ao"``, - ``"rcond_svd"``. + name: One of ``"machine_precision"``, ``"rcond_svd"``. dtype: A NumPy/JAX dtype (e.g. ``jnp.float32``, ``np.float64``). Returns: @@ -126,9 +123,6 @@ def get_eps(name: str, dtype) -> float: return _EPS_DTYPE_AWARE[name][dtype_key] -# Numerical stability settings for AO -EPS_stabilizing_jax_AO_cart_deriv = 1.0e-16 - # Threshold for SVD pseudoinverse of the geminal matrix G. # Singular values below EPS_rcond_SVD * s_max are zeroed to avoid 1/~0 NaN. # Must be very small (e.g. 1e-20); see compute_grads_and_laplacian_ln_Det diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index 4489bf62..22896f3b 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -70,7 +70,7 @@ from ._jqmc_utility import _spherical_to_cart_matrix from ._precision import get_dtype_jnp, get_dtype_np -from ._setting import atol_consistency, get_eps, rtol_consistency +from ._setting import atol_consistency, rtol_consistency from .structure import Structure_data # set logger @@ -1366,6 +1366,7 @@ class ShellPrimMap: __slots__ = ("unique_indices", "prim_to_unique", "num_unique", "num_full") def __init__(self, unique_indices: np.ndarray, prim_to_unique: np.ndarray): + """Build the prim<->unique-AO index mapping from precomputed arrays.""" self.unique_indices = unique_indices self.prim_to_unique = prim_to_unique self.num_unique = len(unique_indices) @@ -2259,16 +2260,15 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A nx_ao = jnp.asarray(aos_data.polynominal_order_x, dtype=jnp.int32) ny_ao = jnp.asarray(aos_data.polynominal_order_y, dtype=jnp.int32) nz_ao = jnp.asarray(aos_data.polynominal_order_z, dtype=jnp.int32) - # NOTE: the previous ``stabilizing_ao`` epsilon (``x + eps``) was needed - # only to guard the autodiff path against ``0**0`` when an electron sat - # exactly on a nucleus. The static-unrolled :func:`_int_pow_unrolled_cart` - # already handles the ``e == 0`` branch via ``where(e == 0, 1.0, base)``, - # making the eps redundant. Removing it eliminates three ``add`` ops and - # the associated select tree from the dominant fusion. Production AD - # reaches AO derivatives only through the analytic kernels - # (``_compute_AOs_grad_analytic_*`` / ``_compute_AOs_laplacian_analytic_*``), - # so this is safe; the autodiff debug variant still benefits from - # ``_int_pow_unrolled_cart``'s ``e == 0`` short-circuit. + # NOTE: the legacy AO stabilizer epsilon (``x + eps``) was needed only + # to guard the kernels against ``0**0`` (here) and ``n/0`` (in the + # analytic grad/lap kernels) when an electron sat exactly on a nucleus. + # The static-unrolled :func:`_int_pow_unrolled_cart` already handles + # ``e == 0`` via ``where(e == 0, 1.0, base)``, and the analytic + # grad/lap kernels were rewritten in shifted-exponent product form + # (``n * x^(n-1)``, ``n(n-1) * x^(n-2)``) so the divisions and + # ``where(base != 0)`` masks are no longer needed either. The eps is + # therefore fully removed across the AO module. P_l_nx_ny_nz_ao = ( _int_pow_unrolled_cart(x_ao, nx_ao, L_MAX) * _int_pow_unrolled_cart(y_ao, ny_ao, L_MAX) @@ -2860,27 +2860,44 @@ def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.n N = jnp.sqrt(N_Z * N_fact) x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] - eps = get_eps("stabilizing_ao", dtype_jnp) - x = x + eps - y = y + eps - z = z + eps r2 = jnp.sum(diff**2, axis=-1) pref = c[:, None] * jnp.exp(-Z[:, None] * r2) - # Static-unrolled integer power avoids the XLA repeated-squaring while-loop - # emitted by ``base ** exp[:, None]`` when ``exp`` is a traced int array. + # See _compute_AOs_grad_analytic_cart for the rationale of the shifted- + # exponent formulation. Here we additionally need + # ``∂²_x x^n = n(n-1) x^(n-2)``, again expressed as + # ``n(n-1) * x^(max(n-2, 0))``; the prefactor zeros the term for + # n < 2 to match the analytic limit. No divisions, no eps, no masks. px = _int_pow_unrolled_cart(x, nx, L_MAX) py = _int_pow_unrolled_cart(y, ny, L_MAX) pz = _int_pow_unrolled_cart(z, nz, L_MAX) - phi = N[:, None] * pref * px * py * pz - - def _second_component(base, n): - safe_div = jnp.where(base != 0.0, n[:, None] / base, 0.0) - safe_div2 = jnp.where(base != 0.0, n[:, None] / (base**2), 0.0) - a = safe_div - 2.0 * Z[:, None] * base - return phi * (a**2 - safe_div2 - 2.0 * Z[:, None]) - - lap_dup = _second_component(x, nx) + _second_component(y, ny) + _second_component(z, nz) + qpx = _int_pow_unrolled_cart(x, jnp.maximum(nx - 1, 0), L_MAX) + qpy = _int_pow_unrolled_cart(y, jnp.maximum(ny - 1, 0), L_MAX) + qpz = _int_pow_unrolled_cart(z, jnp.maximum(nz - 1, 0), L_MAX) + qppx = _int_pow_unrolled_cart(x, jnp.maximum(nx - 2, 0), L_MAX) + qppy = _int_pow_unrolled_cart(y, jnp.maximum(ny - 2, 0), L_MAX) + qppz = _int_pow_unrolled_cart(z, jnp.maximum(nz - 2, 0), L_MAX) + + nx_b = nx[:, None].astype(dtype_jnp) + ny_b = ny[:, None].astype(dtype_jnp) + nz_b = nz[:, None].astype(dtype_jnp) + Npref = N[:, None] * pref + Z_b = Z[:, None] + phi = Npref * px * py * pz + Kx = Npref * py * pz + Ky = Npref * px * pz + Kz = Npref * px * py + + # ∂²_x phi = K_x · [n(n-1) x^(n-2) − 4Z n x^n + (4Z² x² − 2Z) x^n] + # Sum over x,y,z gives the Laplacian. The (4Z² r² − 6Z) term is the + # collected isotropic contribution from the three (4Z² d² − 2Z) pieces. + lap_dup = ( + Kx * (nx_b * (nx_b - 1.0) * qppx) + + Ky * (ny_b * (ny_b - 1.0) * qppy) + + Kz * (nz_b * (nz_b - 1.0) * qppz) + - 4.0 * Z_b * (x * Kx * (nx_b * qpx) + y * Ky * (ny_b * qpy) + z * Kz * (nz_b * qpz)) + + (4.0 * Z_b * Z_b * r2 - 6.0 * Z_b) * phi + ) lap = _reduce_primitives_to_aos(lap_dup, aos_data) return lap @@ -3171,27 +3188,33 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra N = jnp.sqrt(N_Z * N_fact) x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] - eps = get_eps("stabilizing_ao", dtype_jnp) - x = x + eps - y = y + eps - z = z + eps r2 = jnp.sum(diff**2, axis=-1) pref = c[:, None] * jnp.exp(-Z[:, None] * r2) - # Static-unrolled integer power avoids the XLA repeated-squaring while-loop - # emitted by ``base ** exp[:, None]`` when ``exp`` is a traced int array. + # Static-unrolled integer powers. The shifted exponents + # ``max(n - 1, 0)`` combined with the integer prefactor ``n`` express + # ``∂_x x^n = n x^(n-1)`` directly. For ``n == 0`` the prefactor zeros + # the term, matching the analytic limit; this lets us drop both the + # legacy AO stabilizer eps and the ``where(base != 0, n/base, 0)`` + # safeguard, eliminating divisions from the GPU kernel. px = _int_pow_unrolled_cart(x, nx, L_MAX) py = _int_pow_unrolled_cart(y, ny, L_MAX) pz = _int_pow_unrolled_cart(z, nz, L_MAX) - phi = N[:, None] * pref * px * py * pz - - def _grad_component(base, n): - safe_div = jnp.where(base != 0.0, n[:, None] / base, 0.0) - return phi * (safe_div - 2.0 * Z[:, None] * base) - - gx_dup = _grad_component(x, nx) - gy_dup = _grad_component(y, ny) - gz_dup = _grad_component(z, nz) + qpx = _int_pow_unrolled_cart(x, jnp.maximum(nx - 1, 0), L_MAX) + qpy = _int_pow_unrolled_cart(y, jnp.maximum(ny - 1, 0), L_MAX) + qpz = _int_pow_unrolled_cart(z, jnp.maximum(nz - 1, 0), L_MAX) + + nx_b = nx[:, None].astype(dtype_jnp) + ny_b = ny[:, None].astype(dtype_jnp) + nz_b = nz[:, None].astype(dtype_jnp) + Npref = N[:, None] * pref + Z_b = Z[:, None] + phi = Npref * px * py * pz + + # ∂_x phi = (N pref y^(n_y) z^(n_z)) · n_x x^(n_x-1) − 2Z x · phi + gx_dup = Npref * py * pz * (nx_b * qpx) - 2.0 * Z_b * x * phi + gy_dup = Npref * px * pz * (ny_b * qpy) - 2.0 * Z_b * y * phi + gz_dup = Npref * px * py * (nz_b * qpz) - 2.0 * Z_b * z * phi gx = _reduce_primitives_to_aos(gx_dup, aos_data) gy = _reduce_primitives_to_aos(gy_dup, aos_data) @@ -3339,50 +3362,58 @@ def _compute_AOs_value_grad_lap_cart( N = jnp.sqrt(N_Z * N_fact) x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] - eps = get_eps("stabilizing_ao", dtype_jnp) - x = x + eps - y = y + eps - z = z + eps r2 = jnp.sum(diff**2, axis=-1) pref = c[:, None] * jnp.exp(-Z[:, None] * r2) # Static-unrolled integer power avoids the XLA repeated-squaring while-loop # emitted by ``base ** exp[:, None]`` when ``exp`` is a traced int array. # See ``_int_pow_unrolled_cart`` for the bitwise-equivalence rationale. + # Shifted exponents (``max(n - 1, 0)``, ``max(n - 2, 0)``) combined with + # the integer prefactors (``n``, ``n(n-1)``) express the first and + # second derivatives of ``x^n`` in product form, matching the layout + # used by ``_compute_AOs_grad_analytic_cart`` and + # ``_compute_AOs_laplacian_analytic_cart``. Mathematical (not bitwise) + # parity vs those standalone kernels — agreement to ULP magnitude. L_MAX = _cart_max_polynomial_order(aos_data) px = _int_pow_unrolled_cart(x, nx, L_MAX) py = _int_pow_unrolled_cart(y, ny, L_MAX) pz = _int_pow_unrolled_cart(z, nz, L_MAX) - # Shared body identical to the standalone grad/lap kernels (left-to-right - # multiplication). Strict (rtol=atol=0) parity vs compute_AOs_grad and - # compute_AOs_laplacian holds because the expression is bit-for-bit the - # same; parity vs compute_AOs is preserved up to a few ULPs because the - # standalone eval kernel uses a different multiplication ordering. - phi = N[:, None] * pref * px * py * pz # shared val/grad/lap body + qpx = _int_pow_unrolled_cart(x, jnp.maximum(nx - 1, 0), L_MAX) + qpy = _int_pow_unrolled_cart(y, jnp.maximum(ny - 1, 0), L_MAX) + qpz = _int_pow_unrolled_cart(z, jnp.maximum(nz - 1, 0), L_MAX) + qppx = _int_pow_unrolled_cart(x, jnp.maximum(nx - 2, 0), L_MAX) + qppy = _int_pow_unrolled_cart(y, jnp.maximum(ny - 2, 0), L_MAX) + qppz = _int_pow_unrolled_cart(z, jnp.maximum(nz - 2, 0), L_MAX) + + nx_b = nx[:, None].astype(dtype_jnp) + ny_b = ny[:, None].astype(dtype_jnp) + nz_b = nz[:, None].astype(dtype_jnp) + Npref = N[:, None] * pref + Z_b = Z[:, None] + phi = Npref * px * py * pz + Kx = Npref * py * pz + Ky = Npref * px * pz + Kz = Npref * px * py # value finalize: only downcast site (Principle 3b). val = _reduce_primitives_to_aos(phi.astype(dtype_eval), aos_data) # grad finalize (kept in ao_grad_lap zone — no cast). - def _grad_component(base, n): - safe_div = jnp.where(base != 0.0, n[:, None] / base, 0.0) - return phi * (safe_div - 2.0 * Z[:, None] * base) - - gx_dup = _grad_component(x, nx) - gy_dup = _grad_component(y, ny) - gz_dup = _grad_component(z, nz) + gx_dup = Kx * (nx_b * qpx) - 2.0 * Z_b * x * phi + gy_dup = Ky * (ny_b * qpy) - 2.0 * Z_b * y * phi + gz_dup = Kz * (nz_b * qpz) - 2.0 * Z_b * z * phi gx = _reduce_primitives_to_aos(gx_dup, aos_data) gy = _reduce_primitives_to_aos(gy_dup, aos_data) gz = _reduce_primitives_to_aos(gz_dup, aos_data) # lap finalize (kept in ao_grad_lap zone — no cast). - def _second_component(base, n): - safe_div = jnp.where(base != 0.0, n[:, None] / base, 0.0) - safe_div2 = jnp.where(base != 0.0, n[:, None] / (base**2), 0.0) - a = safe_div - 2.0 * Z[:, None] * base - return phi * (a**2 - safe_div2 - 2.0 * Z[:, None]) - - lap_dup = _second_component(x, nx) + _second_component(y, ny) + _second_component(z, nz) + lap_dup = ( + Kx * (nx_b * (nx_b - 1.0) * qppx) + + Ky * (ny_b * (ny_b - 1.0) * qppy) + + Kz * (nz_b * (nz_b - 1.0) * qppz) + - 4.0 * Z_b * (x * Kx * (nx_b * qpx) + y * Ky * (ny_b * qpy) + z * Kz * (nz_b * qpz)) + + (4.0 * Z_b * Z_b * r2 - 6.0 * Z_b) * phi + ) lap = _reduce_primitives_to_aos(lap_dup, aos_data) return val, gx, gy, gz, lap From 27f5435313357948e32029628c615951b1032ec6 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sun, 3 May 2026 16:01:02 +0900 Subject: [PATCH 36/97] Reduce DRAM traffic at the HLO level: AO grad/lap, lambda chains, J3 transpose Three fusion-graph rewrites that eliminate large primitive-rank intermediates from the kinetic-energy / local-energy / LRDMC paths: - compute_AOs*_cart: poly-after-reduce. Three radial moments (NR, ZNR, Z2NR) are reduced to AO rank, and the polynomial derivatives run at AO rank only. Removes the (n_walker, num_ao_prim, n_elec) intermediate that dominated the kinetic-energy HLO. - compute_grads_and_laplacian_ln_Det_fast: chain reorder so the heavy lambda . AO product is shared across gradient and Laplacian assembly instead of being recomputed per component. - _compute_ratio_Jastrow_part_rank1_update / _split_spin: jnp.dot(X.T, Y) replaced with jnp.tensordot(X, Y, axes=((0,),(0,))) in four hot sites; eliminates a transpose materialisation that XLA could not fold under vmap. Removes obsolete helpers _static_int_pow and _build_cart_poly_groups (no remaining call sites). --- jqmc/atomic_orbital.py | 348 ++++++++++++++++++++++++++--------------- jqmc/determinant.py | 48 ++++-- jqmc/jastrow_factor.py | 19 ++- 3 files changed, 271 insertions(+), 144 deletions(-) diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index 22896f3b..85ad44f3 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -2269,6 +2269,17 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A # (``n * x^(n-1)``, ``n(n-1) * x^(n-2)``) so the divisions and # ``where(base != 0)`` masks are no longer needed either. The eps is # therefore fully removed across the AO module. + # + # NOTE (perf, shell-wise unroll attempt 2026-05): grouping AOs by static + # ``(nx, ny, nz)`` triplets and emitting a direct multiply chain per + # group eliminates the L_MAX-deep ``where(e == k, ...)`` select tree, + # but the required permute-once / inverse-permute layout introduced an + # end-of-kernel ``inv_perm`` gather of shape ``(num_ao, n_walker, + # n_elec)`` (~2 GB at f64) that became the new dominant + # ``loop_gather_fusion_1`` kernel on GH200, slowing cart f64 from + # 24 ms → 41 ms (NSYS measured). The select-tree formulation below is + # therefore preferred until a layout that avoids the round-trip gather + # is found. P_l_nx_ny_nz_ao = ( _int_pow_unrolled_cart(x_ao, nx_ao, L_MAX) * _int_pow_unrolled_cart(y_ao, ny_ao, L_MAX) @@ -2837,7 +2848,23 @@ def _single_val_grad_lap(diff: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.A @jit def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> jax.Array: - """Analytic Laplacian for Cartesian AOs (contracted).""" + r"""Analytic Laplacian for Cartesian AOs (contracted). + + Implementation note (perf, poly-after-reduce): + Mirrors the rewrite in ``_compute_AOs_value_grad_lap_cart``. + Three radial moments ``NR``, ``ZNR``, ``Z²NR`` are reduced from + primitive rank to AO rank, then + + .. math:: + \\nabla^2 \\phi = NR \\cdot \\nabla^2 P + - 4\\, ZNR \\cdot (x \\partial_x P + y \\partial_y P + z \\partial_z P) + + (4 r^2\\, Z^2NR - 6\\, ZNR) \\cdot P + + is assembled at AO rank. Eliminates the + ``(n_walker, num_ao_prim, n_elec)`` prim-rank Laplacian + intermediate that previously appeared in the standalone API + path. + """ dtype_jnp = get_dtype_jnp("ao_grad_lap") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). @@ -2848,9 +2875,6 @@ def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.n c = aos_data._coefficients_jnp.astype(dtype_jnp) Z = aos_data._exponents_jnp.astype(dtype_jnp) l = aos_data._angular_momentums_prim_jnp - nx = aos_data._polynominal_order_x_prim_jnp - ny = aos_data._polynominal_order_y_prim_jnp - nz = aos_data._polynominal_order_z_prim_jnp N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) # Static-unrolled (8 Z)**l avoids the XLA repeated-squaring while-loop @@ -2859,47 +2883,48 @@ def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.n N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * _int_pow_unrolled_cart(8.0 * Z, l, L_MAX) N = jnp.sqrt(N_Z * N_fact) - x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] - r2 = jnp.sum(diff**2, axis=-1) - pref = c[:, None] * jnp.exp(-Z[:, None] * r2) - - # See _compute_AOs_grad_analytic_cart for the rationale of the shifted- - # exponent formulation. Here we additionally need - # ``∂²_x x^n = n(n-1) x^(n-2)``, again expressed as - # ``n(n-1) * x^(max(n-2, 0))``; the prefactor zeros the term for - # n < 2 to match the analytic limit. No divisions, no eps, no masks. - px = _int_pow_unrolled_cart(x, nx, L_MAX) - py = _int_pow_unrolled_cart(y, ny, L_MAX) - pz = _int_pow_unrolled_cart(z, nz, L_MAX) - qpx = _int_pow_unrolled_cart(x, jnp.maximum(nx - 1, 0), L_MAX) - qpy = _int_pow_unrolled_cart(y, jnp.maximum(ny - 1, 0), L_MAX) - qpz = _int_pow_unrolled_cart(z, jnp.maximum(nz - 1, 0), L_MAX) - qppx = _int_pow_unrolled_cart(x, jnp.maximum(nx - 2, 0), L_MAX) - qppy = _int_pow_unrolled_cart(y, jnp.maximum(ny - 2, 0), L_MAX) - qppz = _int_pow_unrolled_cart(z, jnp.maximum(nz - 2, 0), L_MAX) - - nx_b = nx[:, None].astype(dtype_jnp) - ny_b = ny[:, None].astype(dtype_jnp) - nz_b = nz[:, None].astype(dtype_jnp) - Npref = N[:, None] * pref + r2_prim = jnp.sum(diff**2, axis=-1) + pref_prim = N[:, None] * c[:, None] * jnp.exp(-Z[:, None] * r2_prim) + + # Three radial moments contracted at primitive rank → reduced to AO rank. Z_b = Z[:, None] - phi = Npref * px * py * pz - Kx = Npref * py * pz - Ky = Npref * px * pz - Kz = Npref * px * py - - # ∂²_x phi = K_x · [n(n-1) x^(n-2) − 4Z n x^n + (4Z² x² − 2Z) x^n] - # Sum over x,y,z gives the Laplacian. The (4Z² r² − 6Z) term is the - # collected isotropic contribution from the three (4Z² d² − 2Z) pieces. - lap_dup = ( - Kx * (nx_b * (nx_b - 1.0) * qppx) - + Ky * (ny_b * (ny_b - 1.0) * qppy) - + Kz * (nz_b * (nz_b - 1.0) * qppz) - - 4.0 * Z_b * (x * Kx * (nx_b * qpx) + y * Ky * (ny_b * qpy) + z * Kz * (nz_b * qpz)) - + (4.0 * Z_b * Z_b * r2 - 6.0 * Z_b) * phi - ) + NR_ao = _reduce_primitives_to_aos(pref_prim, aos_data) + ZNR_ao = _reduce_primitives_to_aos(Z_b * pref_prim, aos_data) + Z2NR_ao = _reduce_primitives_to_aos(Z_b * Z_b * pref_prim, aos_data) - lap = _reduce_primitives_to_aos(lap_dup, aos_data) + # AO-level coordinates. + R_carts_ao = aos_data._atomic_center_carts_jnp + diff_ao = (r_carts[None, :, :] - R_carts_ao[:, None, :]).astype(dtype_jnp) + x, y, z = diff_ao[..., 0], diff_ao[..., 1], diff_ao[..., 2] + r2_ao = jnp.sum(diff_ao**2, axis=-1) + + nx_ao = jnp.asarray(aos_data.polynominal_order_x, dtype=jnp.int32) + ny_ao = jnp.asarray(aos_data.polynominal_order_y, dtype=jnp.int32) + nz_ao = jnp.asarray(aos_data.polynominal_order_z, dtype=jnp.int32) + + # Static-unrolled integer powers at AO rank. See grad analog above for + # rationale of the shifted-exponent formulation; here we additionally + # need ``∂²_x x^n = n(n-1) x^(n-2)``. + px = _int_pow_unrolled_cart(x, nx_ao, L_MAX) + py = _int_pow_unrolled_cart(y, ny_ao, L_MAX) + pz = _int_pow_unrolled_cart(z, nz_ao, L_MAX) + qpx = _int_pow_unrolled_cart(x, jnp.maximum(nx_ao - 1, 0), L_MAX) + qpy = _int_pow_unrolled_cart(y, jnp.maximum(ny_ao - 1, 0), L_MAX) + qpz = _int_pow_unrolled_cart(z, jnp.maximum(nz_ao - 1, 0), L_MAX) + qppx = _int_pow_unrolled_cart(x, jnp.maximum(nx_ao - 2, 0), L_MAX) + qppy = _int_pow_unrolled_cart(y, jnp.maximum(ny_ao - 2, 0), L_MAX) + qppz = _int_pow_unrolled_cart(z, jnp.maximum(nz_ao - 2, 0), L_MAX) + + nx_b = nx_ao[:, None].astype(dtype_jnp) + ny_b = ny_ao[:, None].astype(dtype_jnp) + nz_b = nz_ao[:, None].astype(dtype_jnp) + + P = px * py * pz + lapP = ( + (nx_b * (nx_b - 1.0) * qppx) * py * pz + px * (ny_b * (ny_b - 1.0) * qppy) * pz + px * py * (nz_b * (nz_b - 1.0) * qppz) + ) + rdotgradP = x * (nx_b * qpx) * py * pz + y * px * (ny_b * qpy) * pz + z * px * py * (nz_b * qpz) + lap = NR_ao * lapP - 4.0 * ZNR_ao * rdotgradP + (4.0 * r2_ao * Z2NR_ao - 6.0 * ZNR_ao) * P return lap @@ -3166,7 +3191,23 @@ def _compute_AOs_laplacian_debug( @jit def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarray) -> tuple[jax.Array, jax.Array, jax.Array]: - """Analytic gradients for Cartesian AOs (contracted).""" + r"""Analytic gradients for Cartesian AOs (contracted). + + Implementation note (perf, poly-after-reduce): + Mirrors the rewrite in ``_compute_AOs_value_grad_lap_cart``. + Two radial moments ``NR = Σ_p N_p c_p e^{-Z_p r^2}`` and + ``ZNR = Σ_p Z_p N_p c_p e^{-Z_p r^2}`` are reduced from + primitive rank to AO rank, then + + .. math:: + \\partial_a \\phi = NR \\cdot \\partial_a P - 2 x_a\\, ZNR \\cdot P + + is assembled at AO rank. Eliminates the + ``(n_walker, num_ao_prim, n_elec, 3)`` prim-rank gradient + intermediate (5.5 GB on cc-pVQZ C6H6) that previously dominated + the kinetic_disc / kinetic_continuum HLO when the standalone + grad API is reached (e.g. via ``compute_AOs_grad`` callers). + """ dtype_jnp = get_dtype_jnp("ao_grad_lap") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) # via JAX promotion, then downcast to the ao_grad_lap zone (Principle 3b). @@ -3177,9 +3218,6 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra c = aos_data._coefficients_jnp.astype(dtype_jnp) Z = aos_data._exponents_jnp.astype(dtype_jnp) l = aos_data._angular_momentums_prim_jnp - nx = aos_data._polynominal_order_x_prim_jnp - ny = aos_data._polynominal_order_y_prim_jnp - nz = aos_data._polynominal_order_z_prim_jnp N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) # Static-unrolled (8 Z)**l avoids the XLA repeated-squaring while-loop. @@ -3187,38 +3225,45 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * _int_pow_unrolled_cart(8.0 * Z, l, L_MAX) N = jnp.sqrt(N_Z * N_fact) - x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] - r2 = jnp.sum(diff**2, axis=-1) - pref = c[:, None] * jnp.exp(-Z[:, None] * r2) - - # Static-unrolled integer powers. The shifted exponents - # ``max(n - 1, 0)`` combined with the integer prefactor ``n`` express - # ``∂_x x^n = n x^(n-1)`` directly. For ``n == 0`` the prefactor zeros - # the term, matching the analytic limit; this lets us drop both the - # legacy AO stabilizer eps and the ``where(base != 0, n/base, 0)`` - # safeguard, eliminating divisions from the GPU kernel. - px = _int_pow_unrolled_cart(x, nx, L_MAX) - py = _int_pow_unrolled_cart(y, ny, L_MAX) - pz = _int_pow_unrolled_cart(z, nz, L_MAX) - qpx = _int_pow_unrolled_cart(x, jnp.maximum(nx - 1, 0), L_MAX) - qpy = _int_pow_unrolled_cart(y, jnp.maximum(ny - 1, 0), L_MAX) - qpz = _int_pow_unrolled_cart(z, jnp.maximum(nz - 1, 0), L_MAX) - - nx_b = nx[:, None].astype(dtype_jnp) - ny_b = ny[:, None].astype(dtype_jnp) - nz_b = nz[:, None].astype(dtype_jnp) - Npref = N[:, None] * pref + r2_prim = jnp.sum(diff**2, axis=-1) + pref_prim = N[:, None] * c[:, None] * jnp.exp(-Z[:, None] * r2_prim) + + # Two radial moments contracted at primitive rank → reduced to AO rank. Z_b = Z[:, None] - phi = Npref * px * py * pz + NR_ao = _reduce_primitives_to_aos(pref_prim, aos_data) # Σ_p pref_p + ZNR_ao = _reduce_primitives_to_aos(Z_b * pref_prim, aos_data) # Σ_p Z_p pref_p + + # AO-level coordinates: each AO sits on exactly one atom. + R_carts_ao = aos_data._atomic_center_carts_jnp + diff_ao = (r_carts[None, :, :] - R_carts_ao[:, None, :]).astype(dtype_jnp) + x, y, z = diff_ao[..., 0], diff_ao[..., 1], diff_ao[..., 2] - # ∂_x phi = (N pref y^(n_y) z^(n_z)) · n_x x^(n_x-1) − 2Z x · phi - gx_dup = Npref * py * pz * (nx_b * qpx) - 2.0 * Z_b * x * phi - gy_dup = Npref * px * pz * (ny_b * qpy) - 2.0 * Z_b * y * phi - gz_dup = Npref * px * py * (nz_b * qpz) - 2.0 * Z_b * z * phi + # AO-rank polynomial orders (length num_ao). Static, constant-folded. + nx_ao = jnp.asarray(aos_data.polynominal_order_x, dtype=jnp.int32) + ny_ao = jnp.asarray(aos_data.polynominal_order_y, dtype=jnp.int32) + nz_ao = jnp.asarray(aos_data.polynominal_order_z, dtype=jnp.int32) + + # Static-unrolled integer powers at AO rank. Shifted exponent + # ``max(n - 1, 0)`` combined with prefactor ``n`` expresses + # ``∂_x x^n = n x^(n-1)`` directly; for ``n == 0`` the prefactor zeros + # the term, matching the analytic limit (no eps, no divisions). + px = _int_pow_unrolled_cart(x, nx_ao, L_MAX) + py = _int_pow_unrolled_cart(y, ny_ao, L_MAX) + pz = _int_pow_unrolled_cart(z, nz_ao, L_MAX) + qpx = _int_pow_unrolled_cart(x, jnp.maximum(nx_ao - 1, 0), L_MAX) + qpy = _int_pow_unrolled_cart(y, jnp.maximum(ny_ao - 1, 0), L_MAX) + qpz = _int_pow_unrolled_cart(z, jnp.maximum(nz_ao - 1, 0), L_MAX) + + nx_b = nx_ao[:, None].astype(dtype_jnp) + ny_b = ny_ao[:, None].astype(dtype_jnp) + nz_b = nz_ao[:, None].astype(dtype_jnp) + + P = px * py * pz # (num_ao, n_elec) - gx = _reduce_primitives_to_aos(gx_dup, aos_data) - gy = _reduce_primitives_to_aos(gy_dup, aos_data) - gz = _reduce_primitives_to_aos(gz_dup, aos_data) + # ∂_a φ = NR · ∂_a P − 2 x_a · ZNR · P + gx = NR_ao * ((nx_b * qpx) * py * pz) - 2.0 * x * ZNR_ao * P + gy = NR_ao * (px * (ny_b * qpy) * pz) - 2.0 * y * ZNR_ao * P + gz = NR_ao * (px * py * (nz_b * qpz)) - 2.0 * z * ZNR_ao * P return gx, gy, gz @@ -3333,12 +3378,42 @@ def compute_AOs_grad(aos_data: AOs_sphe_data | AOs_cart_data, r_carts: jax.Array def _compute_AOs_value_grad_lap_cart( aos_data: AOs_cart_data, r_carts: jnp.ndarray ) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array, jax.Array]: - """Fused value/grad/lap for Cartesian AOs (contracted). + r"""Fused value/grad/lap for Cartesian AOs (contracted). - Shared heavy block (``exp(-Z r^2)``, polynomial powers, ``phi``) is - evaluated once in the ``ao_grad_lap`` zone (fp64); only the value - output is downcast to ``ao_eval`` at the segment-sum site. See module - docstring for the full rationale. + Implementation note (perf, poly-after-reduce for grad/lap): + The angular polynomial part :math:`x^{n_x} y^{n_y} z^{n_z}` and + its derivatives depend only on AO-level quantum numbers, not on + the primitive index. Within an AO, every primitive shares the + same ``(n_x, n_y, n_z)`` and the same Cartesian center. Writing + + .. math:: + \\mathrm{pref}_p = N_p c_p e^{-Z_p r^2}, \\qquad + NR = \\sum_p \\mathrm{pref}_p, \\qquad + ZNR = \\sum_p Z_p\\, \\mathrm{pref}_p, \\qquad + Z^2NR = \\sum_p Z_p^2\\, \\mathrm{pref}_p, + + all primitive-rank arrays disappear after three radial reductions + — value, grad and Laplacian then reduce to AO-rank polynomial + algebra: + + .. math:: + \\phi &= NR \\cdot P \\\\ + \\partial_a \\phi &= NR \\cdot \\partial_a P - 2 x_a\\, ZNR \\cdot P \\\\ + \\nabla^2 \\phi &= NR \\cdot \\nabla^2 P + - 4\\, ZNR \\cdot (x \\partial_x P + y \\partial_y P + z \\partial_z P) + + (4 r^2\\, Z^2NR - 6\\, ZNR) \\cdot P + + Previously the kernel materialised ``phi``, ``Kx``, ``Ky``, ``Kz`` + as four ``(n_walker, num_ao_prim, n_elec)`` tuples (~7.4 GB on + cc-pVQZ C6H6) and reduced each to AO rank with a separate + ``_reduce_primitives_to_aos``; HLO dump showed a single + ``loop_multiply_reduce_select_fusion`` of type + ``(f64[8192,880,32], f64[8192,880,32], f64[8192,880,32], + f64[8192,880,32])`` — the dominant DRAM consumer of the kinetic + energy / local energy paths. This rewrite shrinks that to one + prim-rank reduce per radial moment (NR / ZNR / Z²NR) and runs + all polynomial work at AO rank (~144 channel for C6H6 vs 880 + prim). """ dtype_eval = get_dtype_jnp("ao_eval") dtype_jnp = get_dtype_jnp("ao_grad_lap") @@ -3351,9 +3426,6 @@ def _compute_AOs_value_grad_lap_cart( c = aos_data._coefficients_jnp.astype(dtype_jnp) Z = aos_data._exponents_jnp.astype(dtype_jnp) l = aos_data._angular_momentums_prim_jnp - nx = aos_data._polynominal_order_x_prim_jnp - ny = aos_data._polynominal_order_y_prim_jnp - nz = aos_data._polynominal_order_z_prim_jnp N_fact = aos_data._normalization_factorial_ratio_prim_jnp.astype(dtype_jnp) # Static-unrolled (8 Z)**l avoids the XLA repeated-squaring while-loop. @@ -3361,60 +3433,76 @@ def _compute_AOs_value_grad_lap_cart( N_Z = (2.0 * Z / jnp.pi) ** (3.0 / 2.0) * _int_pow_unrolled_cart(8.0 * Z, l, L_MAX) N = jnp.sqrt(N_Z * N_fact) - x, y, z = diff[..., 0], diff[..., 1], diff[..., 2] - r2 = jnp.sum(diff**2, axis=-1) - pref = c[:, None] * jnp.exp(-Z[:, None] * r2) - - # Static-unrolled integer power avoids the XLA repeated-squaring while-loop - # emitted by ``base ** exp[:, None]`` when ``exp`` is a traced int array. - # See ``_int_pow_unrolled_cart`` for the bitwise-equivalence rationale. - # Shifted exponents (``max(n - 1, 0)``, ``max(n - 2, 0)``) combined with - # the integer prefactors (``n``, ``n(n-1)``) express the first and - # second derivatives of ``x^n`` in product form, matching the layout - # used by ``_compute_AOs_grad_analytic_cart`` and - # ``_compute_AOs_laplacian_analytic_cart``. Mathematical (not bitwise) - # parity vs those standalone kernels — agreement to ULP magnitude. - L_MAX = _cart_max_polynomial_order(aos_data) - px = _int_pow_unrolled_cart(x, nx, L_MAX) - py = _int_pow_unrolled_cart(y, ny, L_MAX) - pz = _int_pow_unrolled_cart(z, nz, L_MAX) - qpx = _int_pow_unrolled_cart(x, jnp.maximum(nx - 1, 0), L_MAX) - qpy = _int_pow_unrolled_cart(y, jnp.maximum(ny - 1, 0), L_MAX) - qpz = _int_pow_unrolled_cart(z, jnp.maximum(nz - 1, 0), L_MAX) - qppx = _int_pow_unrolled_cart(x, jnp.maximum(nx - 2, 0), L_MAX) - qppy = _int_pow_unrolled_cart(y, jnp.maximum(ny - 2, 0), L_MAX) - qppz = _int_pow_unrolled_cart(z, jnp.maximum(nz - 2, 0), L_MAX) - - nx_b = nx[:, None].astype(dtype_jnp) - ny_b = ny[:, None].astype(dtype_jnp) - nz_b = nz[:, None].astype(dtype_jnp) - Npref = N[:, None] * pref + r2_prim = jnp.sum(diff**2, axis=-1) + pref_prim = N[:, None] * c[:, None] * jnp.exp(-Z[:, None] * r2_prim) + + # Three radial moments contracted at primitive rank → reduced to AO rank + # via a single _reduce_primitives_to_aos each. Everything downstream + # operates at AO rank (num_ao << num_ao_prim), eliminating the + # (n_walker, num_ao_prim, n_elec)-sized intermediate tuple that + # dominated the kinetic energy HLO. Z_b = Z[:, None] - phi = Npref * px * py * pz - Kx = Npref * py * pz - Ky = Npref * px * pz - Kz = Npref * px * py + NR_ao = _reduce_primitives_to_aos(pref_prim, aos_data) # Σ_p pref_p + ZNR_ao = _reduce_primitives_to_aos(Z_b * pref_prim, aos_data) # Σ_p Z_p pref_p + Z2NR_ao = _reduce_primitives_to_aos(Z_b * Z_b * pref_prim, aos_data) # Σ_p Z_p^2 pref_p + + # AO-level coordinates: each AO sits on exactly one atom, so we use the + # AO→atom mapping (length num_ao). r-R is reconstructed in fp64 then + # cast to the ao_grad_lap zone — same protocol as the prim-rank diff + # above, ensuring bit-exact match between the two coordinate sources + # whenever an electron sits on a nucleus. + R_carts_ao = aos_data._atomic_center_carts_jnp + diff_ao = (r_carts[None, :, :] - R_carts_ao[:, None, :]).astype(dtype_jnp) + x, y, z = diff_ao[..., 0], diff_ao[..., 1], diff_ao[..., 2] + r2_ao = jnp.sum(diff_ao**2, axis=-1) + + # AO-rank polynomial orders (length num_ao). These are static lists on + # the dataclass; ``jnp.asarray`` is constant-folded into the JIT. + nx_ao = jnp.asarray(aos_data.polynominal_order_x, dtype=jnp.int32) + ny_ao = jnp.asarray(aos_data.polynominal_order_y, dtype=jnp.int32) + nz_ao = jnp.asarray(aos_data.polynominal_order_z, dtype=jnp.int32) + + # Static-unrolled integer powers at AO rank. Shifted exponents + # (``max(n - 1, 0)``, ``max(n - 2, 0)``) combined with the integer + # prefactors (``n``, ``n(n-1)``) express the first and second + # derivatives of ``x^n`` in product form, matching the layout used + # by the standalone analytic grad/lap kernels. + px = _int_pow_unrolled_cart(x, nx_ao, L_MAX) + py = _int_pow_unrolled_cart(y, ny_ao, L_MAX) + pz = _int_pow_unrolled_cart(z, nz_ao, L_MAX) + qpx = _int_pow_unrolled_cart(x, jnp.maximum(nx_ao - 1, 0), L_MAX) + qpy = _int_pow_unrolled_cart(y, jnp.maximum(ny_ao - 1, 0), L_MAX) + qpz = _int_pow_unrolled_cart(z, jnp.maximum(nz_ao - 1, 0), L_MAX) + qppx = _int_pow_unrolled_cart(x, jnp.maximum(nx_ao - 2, 0), L_MAX) + qppy = _int_pow_unrolled_cart(y, jnp.maximum(ny_ao - 2, 0), L_MAX) + qppz = _int_pow_unrolled_cart(z, jnp.maximum(nz_ao - 2, 0), L_MAX) + + nx_b = nx_ao[:, None].astype(dtype_jnp) + ny_b = ny_ao[:, None].astype(dtype_jnp) + nz_b = nz_ao[:, None].astype(dtype_jnp) + + P = px * py * pz # (num_ao, n_elec) # value finalize: only downcast site (Principle 3b). - val = _reduce_primitives_to_aos(phi.astype(dtype_eval), aos_data) + val = (NR_ao * P).astype(dtype_eval) # grad finalize (kept in ao_grad_lap zone — no cast). - gx_dup = Kx * (nx_b * qpx) - 2.0 * Z_b * x * phi - gy_dup = Ky * (ny_b * qpy) - 2.0 * Z_b * y * phi - gz_dup = Kz * (nz_b * qpz) - 2.0 * Z_b * z * phi - gx = _reduce_primitives_to_aos(gx_dup, aos_data) - gy = _reduce_primitives_to_aos(gy_dup, aos_data) - gz = _reduce_primitives_to_aos(gz_dup, aos_data) + # ∂_a φ = NR · ∂_a P − 2 x_a · ZNR · P, with + # ∂_x P = n_x x^{n_x-1} y^{n_y} z^{n_z}, etc. + gx = NR_ao * ((nx_b * qpx) * py * pz) - 2.0 * x * ZNR_ao * P + gy = NR_ao * (px * (ny_b * qpy) * pz) - 2.0 * y * ZNR_ao * P + gz = NR_ao * (px * py * (nz_b * qpz)) - 2.0 * z * ZNR_ao * P # lap finalize (kept in ao_grad_lap zone — no cast). - lap_dup = ( - Kx * (nx_b * (nx_b - 1.0) * qppx) - + Ky * (ny_b * (ny_b - 1.0) * qppy) - + Kz * (nz_b * (nz_b - 1.0) * qppz) - - 4.0 * Z_b * (x * Kx * (nx_b * qpx) + y * Ky * (ny_b * qpy) + z * Kz * (nz_b * qpz)) - + (4.0 * Z_b * Z_b * r2 - 6.0 * Z_b) * phi + # ∇²P = Σ_a n_a(n_a-1) x_a^{n_a-2} Π_{b≠a} x_b^{n_b} + lapP = ( + (nx_b * (nx_b - 1.0) * qppx) * py * pz + px * (ny_b * (ny_b - 1.0) * qppy) * pz + px * py * (nz_b * (nz_b - 1.0) * qppz) ) - lap = _reduce_primitives_to_aos(lap_dup, aos_data) + # x·∂_xP + y·∂_yP + z·∂_zP = (n_x + n_y + n_z) · P (Euler identity); + # but keep the explicit form for bit-exact match with the legacy + # prim-rank rewrite when arithmetic order matters. + rdotgradP = x * (nx_b * qpx) * py * pz + y * px * (ny_b * qpy) * pz + z * px * py * (nz_b * qpz) + lap = NR_ao * lapP - 4.0 * ZNR_ao * rdotgradP + (4.0 * r2_ao * Z2NR_ao - 6.0 * ZNR_ao) * P return val, gx, gy, gz, lap diff --git a/jqmc/determinant.py b/jqmc/determinant.py index f1fba6f4..e20501f1 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -1611,21 +1611,39 @@ def _compute_ratio_determinant_part_rank1_update( orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts).astype(dtype_jnp) orb_matrix_dn_old = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, old_r_dn_carts).astype(dtype_jnp) - # Batched AO for moved electrons (up) -> rows + # Batched AO for moved electrons (up) -> rows. + # + # R3 (associativity): the chain is + # row_paired = (orb_up_new^T @ lambda_paired) @ orb_dn_old. + # Naively materialising ``orb_up_new^T @ lambda_paired`` produces a + # ``(G, n_orb_dn)`` intermediate that is enormous on the ECP / discretized + # kinetic mesh (G ≈ walker * Nv * NN or walker * 6 * n_elec, easily 1-100 M + # rows for f64 at GH200 scale). Pre-contracting on the small side instead, + # M_paired := lambda_paired @ orb_dn_old # (n_orb_up, N_dn) + # row_paired = orb_up_new^T @ M_paired # (G, N_dn) + # turns the big middle gemm into a small ``(n_orb_up, n_orb_dn) x (n_orb_dn, N_dn)`` + # constant-size product (executed once per walker) followed by a single + # ``(G, n_orb_up) x (n_orb_up, N_dn)`` gemm whose output is the final + # small ``(G, N_dn)`` row block. Roughly (n_orb + N) / N FLOPs are saved + # and the (G, n_orb) intermediate is eliminated, lifting the gemm AI from + # the DRAM-bound regime to ridge. orb_up_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_new_flat).astype( dtype_jnp ) # (n_orb_up, G) - tmp_up = jnp.dot(orb_up_new_batch.T, lambda_matrix_paired) # (G, n_orb_dn) - row_paired = jnp.dot(tmp_up, orb_matrix_dn_old) # (G, N_dn) + M_paired_up = jnp.dot(lambda_matrix_paired, orb_matrix_dn_old) # (n_orb_up, N_dn) + row_paired = jnp.dot(orb_up_new_batch.T, M_paired_up) # (G, N_dn) row_unpaired = jnp.dot(orb_up_new_batch.T, lambda_matrix_unpaired) # (G, num_unpaired) new_rows_up = jnp.hstack([row_paired, row_unpaired]) # (G, N_up) - # Batched AO for moved electrons (dn) -> columns + # Batched AO for moved electrons (dn) -> columns. + # Same R3 reorder for the dn block: + # cols = orb_up_old^T @ (lambda_paired @ orb_dn_new) -> (orb_up_old^T @ lambda_paired) @ orb_dn_new. + # ``M_dn`` has shape ``(N_up, n_orb_dn)`` (small) instead of ``(n_orb_up, G)``. orb_dn_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_new_flat).astype( dtype_jnp ) # (n_orb_dn, G) - w_batch = jnp.dot(lambda_matrix_paired, orb_dn_new_batch) # (n_orb_up, G) - cols = jnp.dot(orb_matrix_up_old.T, w_batch) # (N_up, G) + M_paired_dn = jnp.dot(orb_matrix_up_old.T, lambda_matrix_paired) # (N_up, n_orb_dn) + cols = jnp.dot(M_paired_dn, orb_dn_new_batch) # (N_up, G) new_cols_dn = cols.T # (G, N_up) # rank-1 determinant ratios for up-move grids and dn-move grids. @@ -1723,11 +1741,19 @@ def _compute_ratio_determinant_part_split_spin( r_up_new_flat = jnp.take_along_axis(new_r_up_shifted, idx_up[:, None, None], axis=1).reshape(-1, 3) # Only evaluate up-spin MOs for the moved electron positions. + # + # R3 (associativity): pre-contract on the small side + # M_paired := lambda_paired @ orb_dn_old # (n_orb_up, N_dn) + # so the chain ``(orb_up_new^T @ lambda_paired) @ orb_dn_old`` becomes + # row_paired = orb_up_new^T @ M_paired # (G_up, N_dn) + # eliminating the (G_up, n_orb_dn) middle intermediate. See the matching + # comment in ``_compute_ratio_determinant_part_rank1_update`` for the + # GH200 motivation (DRAM-bound pattern C). orb_up_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_new_flat).astype( dtype_jnp ) # (n_orb_up, G_up) - tmp_up = jnp.dot(orb_up_new_batch.T, lambda_matrix_paired) # (G_up, n_orb_dn) - row_paired = jnp.dot(tmp_up, orb_matrix_dn_old) # (G_up, N_dn) + M_paired_up = jnp.dot(lambda_matrix_paired, orb_matrix_dn_old) # (n_orb_up, N_dn) + row_paired = jnp.dot(orb_up_new_batch.T, M_paired_up) # (G_up, N_dn) row_unpaired = jnp.dot(orb_up_new_batch.T, lambda_matrix_unpaired) # (G_up, num_unpaired) new_rows_up = jnp.hstack([row_paired, row_unpaired]) # (G_up, N_up) @@ -1743,11 +1769,13 @@ def _compute_ratio_determinant_part_split_spin( r_dn_new_flat = jnp.take_along_axis(new_r_dn_shifted, idx_dn[:, None, None], axis=1).reshape(-1, 3) # Only evaluate dn-spin MOs for the moved electron positions. + # R3 (associativity): pre-contract ``M_dn := orb_up_old^T @ lambda_paired`` + # (N_up, n_orb_dn), then ``cols.T = orb_dn_new^T @ M_dn^T = (M_dn @ orb_dn_new).T``. orb_dn_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_new_flat).astype( dtype_jnp ) # (n_orb_dn, G_dn) - w_batch = jnp.dot(lambda_matrix_paired, orb_dn_new_batch) # (n_orb_up, G_dn) - new_cols_dn = jnp.dot(orb_matrix_up_old.T, w_batch).T # (G_dn, N_up) + M_paired_dn = jnp.dot(orb_matrix_up_old.T, lambda_matrix_paired) # (N_up, n_orb_dn) + new_cols_dn = jnp.dot(M_paired_dn, orb_dn_new_batch).T # (G_dn, N_up) A_row_for_dn = jnp.take(A_old_inv_z, idx_dn, axis=0) # (G_dn, N_up) det_ratio_dn_block = jnp.sum(A_row_for_dn * new_cols_dn, axis=1) # (G_dn,) diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 3913231e..ae7e47b8 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -2637,7 +2637,13 @@ def _batch_pairwise_sum(points_a, points_b, param): term1 = j1_vec @ aos_p_batch # (N,) # UP formula ----------------------------------------------------------- - V_up = jnp.dot(aos_p_batch.T, W_up) # (N, N_up) + # Use tensordot with explicit contracting axes (n_ao = axis 0 of both + # operands) instead of ``aos_p_batch.T @ W_up``: under vmap on the + # walker axis XLA's transpose-folding does not fold the ``.T`` into + # the dot, materialising an explicit ~1.8 GB ``transpose`` kernel + # (HBM-bound, ~88-92% DRAM peak on GH200). Expressing the contraction + # via ``dot_general(contract=[0]x[0])`` avoids the materialisation. + V_up = jnp.tensordot(aos_p_batch, W_up, axes=((0,), (0,))) # (N, N_up) P_up = jnp.dot(U_up, aos_p_batch) # (N_up, N) Q_up_c = (idx_for_Q[:, None] < jnp.arange(num_up)[None, :]).astype(dtype_jnp) # (N, N_up) Q_up_r = (idx_for_Q[:, None] > jnp.arange(num_up)[None, :]).astype(dtype_jnp) # (N, N_up) @@ -2647,7 +2653,8 @@ def _batch_pairwise_sum(points_a, points_b, param): J3_log_up = term1 + term2_up + term3_up + term4_up # DN formula ----------------------------------------------------------- - V_dn = jnp.dot(aos_p_batch.T, W_dn) # (N, N_dn) + # See UP-formula comment above re: tensordot-vs-``.T``-then-dot. + V_dn = jnp.tensordot(aos_p_batch, W_dn, axes=((0,), (0,))) # (N, N_dn) P_dn = jnp.dot(U_dn, aos_p_batch) # (N_dn, N) Q_dn_c = (idx_for_Q[:, None] < jnp.arange(num_dn)[None, :]).astype(dtype_jnp) # (N, N_dn) Q_dn_r = (idx_for_Q[:, None] > jnp.arange(num_dn)[None, :]).astype(dtype_jnp) # (N, N_dn) @@ -2899,7 +2906,10 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: aos_p_up = aos_up_new_moved - aos_up_old_moved # (n_ao, G_up) term1_up = j1_vec @ aos_p_up # (G_up,) - V_up_block = jnp.dot(aos_p_up.T, W_up) # (G_up, N_up) + # tensordot avoids the explicit transpose of ``aos_p_up`` (1.8 GB on + # GH200 ECP-nonlocal benchmark) — see ``_compute_ratio_Jastrow_part_rank1_update`` + # for the same rewrite rationale. + V_up_block = jnp.tensordot(aos_p_up, W_up, axes=((0,), (0,))) # (G_up, N_up) P_up_block = jnp.dot(U_up, aos_p_up) # (N_up, G_up) Q_up_c = (idx_up_block[:, None] < jnp.arange(num_up)[None, :]).astype(dtype_jnp) # (G_up, N_up) Q_up_r = (idx_up_block[:, None] > jnp.arange(num_up)[None, :]).astype(dtype_jnp) # (G_up, N_up) @@ -2915,7 +2925,8 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: aos_p_dn = aos_dn_new_moved - aos_dn_old_moved # (n_ao, G_dn) term1_dn = j1_vec @ aos_p_dn # (G_dn,) - V_dn_block = jnp.dot(aos_p_dn.T, W_dn) # (G_dn, N_dn) + # See UP-block comment re: tensordot. + V_dn_block = jnp.tensordot(aos_p_dn, W_dn, axes=((0,), (0,))) # (G_dn, N_dn) P_dn_block = jnp.dot(U_dn, aos_p_dn) # (N_dn, G_dn) Q_dn_c = (idx_dn_block[:, None] < jnp.arange(num_dn)[None, :]).astype(dtype_jnp) # (G_dn, N_dn) Q_dn_r = (idx_dn_block[:, None] > jnp.arange(num_dn)[None, :]).astype(dtype_jnp) # (G_dn, N_dn) From 2c8c233b559233626e51bb6b132859c24facd878 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sun, 3 May 2026 22:33:07 +0900 Subject: [PATCH 37/97] GFMC_t: add streaming kinetic-state path (parity with GFMC_n) Mirror the streaming implementation already in GFMC_n: - Extract _projection_t_core: shared body parameterized by per-electron kinetic energies (ke_up, ke_dn) and an optional j3_state. Returns the legacy tuple plus (has_up_move, up_index, dn_index) for the streaming wrapper. - _projection_t becomes a thin legacy wrapper that recomputes kinetic energies via compute_kinetic_energy_all_elements_fast_update and passes j3_state=None. - _projection_t_streaming is a thin streaming wrapper that reads ke_up/ke_dn from a maintained Kinetic_streaming_state, threads kinetic_state.j3_state into the fast-update kernels (discretized kinetic / non-local ECP / rank-1 Jastrow ratio), and advances the state at the end of each step. - GFMC_t.run() initializes the per-walker streaming state before the projection while-loop and dispatches at the vmap call site via a Python-static use_streaming flag (renamed from use_streaming_t for consistency with GFMC_n). --- jqmc/jqmc_gfmc.py | 289 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 230 insertions(+), 59 deletions(-) diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 3000abbe..1ed18e0a 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -715,9 +715,13 @@ def _generate_rotation_matrix_t(alpha, beta, gamma): ) return R - # Note: This jit drastically accelarates the computation!! - @partial(jit, static_argnums=(7, 8, 9)) - def _projection_t( + # Shared core of legacy and streaming GFMC_t projection. Two thin + # wrappers (`_projection_t` legacy / `_projection_t_streaming`) below + # supply the per-electron continuum kinetic energy and the optional + # ``j3_state`` (None for legacy, ``kinetic_state.j3_state`` for + # streaming) and call this body. Mirrors the ``_body_step_core`` / + # ``_body_fun_n`` / ``_body_fun_n_streaming`` split in GFMC_n. + def _projection_t_core( projection_counter: int, tau_left: float, w_L: float, @@ -725,37 +729,26 @@ def _projection_t( r_dn_carts: jnpt.ArrayLike, A_old_inv: jnpt.ArrayLike, jax_PRNG_key: jnpt.ArrayLike, + diagonal_kinetic_continuum_elements_up: jnpt.ArrayLike, + diagonal_kinetic_continuum_elements_dn: jnpt.ArrayLike, + j3_state, random_discretized_mesh: bool, non_local_move: bool, alat: float, hamiltonian_data: Hamiltonian_data, ): - """Do projection, compatible with vmap. - - Do projection for a set of (r_up_cart, r_dn_cart). - - Args: - projection_counter(int): the counter of projection steps - tau_left (float): left projection time - w_L (float): weight before projection - r_up_carts (N_e^up, 3) before projection - r_dn_carts (N_e^dn, 3) after projection - jax_PRNG_key (jnpt.ArrayLike): jax PRNG key - random_discretized_mesh (bool): Flag for the random discretization mesh in the kinetic part and the non-local part of ECPs. - non_local_move (bool): treatment of the spin-flip term. tmove (Casula's T-move) or dtmove (Determinant Locality Approximation with Casula's T-move) - alat (float): discretized grid length (bohr) - hamiltonian_data (Hamiltonian_data): an instance of Hamiltonian_data - - Returns: - e_L (float): e_L after the final projection. - projection_counter(int): the counter of projection steps - tau_left (float): left projection time - w_L (float): weight after the final projection - r_up_carts (N_e^up, 3) after the final projection - r_dn_carts (N_e^dn, 3) after the final projection - A_old_inv: cached inverse geminal matrix after the final projection - jax_PRNG_key (jnpt.ArrayLike): jax PRNG key - R.T: rotation matrix used for the discretized mesh + """Single GFMC_t projection step, parameterized by per-electron continuum kinetic energy. + + Extracted from the original ``_projection_t`` so that legacy and + streaming wrappers can share the body. The caller is responsible + for supplying the continuum per-electron kinetic energies (legacy: + ``compute_kinetic_energy_all_elements_fast_update``; streaming: + ``_kinetic_energy_from_streaming_state``) and ``j3_state`` (legacy: + None; streaming: the maintained J3 sub-state). + + Returns the same tuple as the legacy ``_projection_t`` plus three + extra fields (``has_up_move``, ``up_index``, ``dn_index``) that the + streaming wrapper uses to drive ``_advance_kinetic_energy_*``. """ # projection counter projection_counter = lax.cond( @@ -769,15 +762,9 @@ def _projection_t( # compute diagonal elements, kinetic part diagonal_kinetic_part = 3.0 / (2.0 * alat**2) * (len(r_up_carts) + len(r_dn_carts)) - # compute continuum kinetic energy - diagonal_kinetic_continuum_elements_up, diagonal_kinetic_continuum_elements_dn = ( - compute_kinetic_energy_all_elements_fast_update( - wavefunction_data=hamiltonian_data.wavefunction_data, - r_up_carts=r_up_carts, - r_dn_carts=r_dn_carts, - geminal_inverse=A_old_inv, - ) - ) + # continuum kinetic energy is supplied by the wrapper (legacy: + # ``compute_kinetic_energy_all_elements_fast_update``; streaming: + # ``_kinetic_energy_from_streaming_state``). # generate a random rotation matrix jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) @@ -798,6 +785,7 @@ def _projection_t( r_up_carts=r_up_carts, r_dn_carts=r_dn_carts, RT=R.T, + j3_state=j3_state, ) ) # spin-filp @@ -920,6 +908,7 @@ def _projection_t( flag_determinant_only=False, A_old_inv=A_old_inv, RT=R.T, + j3_state=j3_state, ) ) @@ -938,6 +927,7 @@ def _projection_t( flag_determinant_only=True, A_old_inv=A_old_inv, RT=R.T, + j3_state=j3_state, ) ) @@ -950,6 +940,7 @@ def _projection_t( old_r_dn_carts=r_dn_carts, new_r_up_carts_arr=mesh_non_local_ecp_part_r_up_carts, new_r_dn_carts_arr=mesh_non_local_ecp_part_r_dn_carts, + j3_state=j3_state, ) V_nonlocal_FN = V_nonlocal_FN * Jastrow_ratio @@ -1110,8 +1101,122 @@ def _update_inv_dn_t(_): A_new_inv, jax_PRNG_key, R.T, + has_up_move, + up_index, + dn_index, ) + # Note: This jit drastically accelarates the computation!! + @partial(jit, static_argnums=(7, 8, 9)) + def _projection_t( + projection_counter: int, + tau_left: float, + w_L: float, + r_up_carts: jnpt.ArrayLike, + r_dn_carts: jnpt.ArrayLike, + A_old_inv: jnpt.ArrayLike, + jax_PRNG_key: jnpt.ArrayLike, + random_discretized_mesh: bool, + non_local_move: bool, + alat: float, + hamiltonian_data: Hamiltonian_data, + ): + """Legacy GFMC_t projection step (no streaming kinetic-energy state). + + Recomputes the per-electron continuum kinetic energy fresh each step + via :func:`compute_kinetic_energy_all_elements_fast_update` and + delegates the rest of the body to :func:`_projection_t_core`. + """ + ke_up, ke_dn = compute_kinetic_energy_all_elements_fast_update( + wavefunction_data=hamiltonian_data.wavefunction_data, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, + geminal_inverse=A_old_inv, + ) + (e_L, pc, tl, wL, ru, rd, Ainv, key, RT, _has_up, _up_idx, _dn_idx) = _projection_t_core( + projection_counter, + tau_left, + w_L, + r_up_carts, + r_dn_carts, + A_old_inv, + jax_PRNG_key, + ke_up, + ke_dn, + None, # j3_state + random_discretized_mesh, + non_local_move, + alat, + hamiltonian_data, + ) + return (e_L, pc, tl, wL, ru, rd, Ainv, key, RT) + + @partial(jit, static_argnums=(8, 9, 10)) + def _projection_t_streaming( + projection_counter: int, + tau_left: float, + w_L: float, + r_up_carts: jnpt.ArrayLike, + r_dn_carts: jnpt.ArrayLike, + A_old_inv: jnpt.ArrayLike, + jax_PRNG_key: jnpt.ArrayLike, + kinetic_state: Kinetic_streaming_state, + random_discretized_mesh: bool, + non_local_move: bool, + alat: float, + hamiltonian_data: Hamiltonian_data, + ): + """Streaming GFMC_t projection step. + + Reads per-electron kinetic energies from ``kinetic_state`` instead + of recomputing them, threads ``kinetic_state.j3_state`` into the + fast-update kernels (discretized kinetic / non-local ECP / rank-1 + Jastrow ratio), delegates the body to :func:`_projection_t_core`, + then advances the kinetic streaming state to the post-step + ``(r_up_new, r_dn_new, A_new_inv)``. + + Valid only when ``jastrow_data.jastrow_nn_data is None`` (NN J3 + has no rank-1 advance). Dispatch is Python-static at the + ``run()`` entry point. When ``tau_left <= 0.0`` the move is + suppressed (positions unchanged, A_new_inv == A_old_inv), so the + advance is a numerical no-op. + """ + ke_up, ke_dn = _kinetic_energy_from_streaming_state(kinetic_state) + (e_L, pc, tl, wL, ru, rd, Ainv, key, RT, has_up, up_idx, dn_idx) = _projection_t_core( + projection_counter, + tau_left, + w_L, + r_up_carts, + r_dn_carts, + A_old_inv, + jax_PRNG_key, + ke_up, + ke_dn, + kinetic_state.j3_state, + random_discretized_mesh, + non_local_move, + alat, + hamiltonian_data, + ) + moved_spin_is_up = has_up + moved_index = jnp.where(has_up, up_idx, dn_idx) + kinetic_state_new = _advance_kinetic_energy_all_elements_streaming_state( + wavefunction_data=hamiltonian_data.wavefunction_data, + state=kinetic_state, + moved_spin_is_up=moved_spin_is_up, + moved_index=moved_index, + r_up_carts_new=ru, + r_dn_carts_new=rd, + A_new_inv=Ainv, + ) + return (e_L, pc, tl, wL, ru, rd, Ainv, kinetic_state_new, key, RT) + + # Python-static dispatch: streaming is incompatible with NN three-body + # Jastrow (J_NN has no rank-1 advance), and offers no benefit when J3 is + # absent. Mirrors the GFMC_n dispatch policy. + jastrow_data = self.__hamiltonian_data.wavefunction_data.jastrow_data + use_streaming = jastrow_data.jastrow_nn_data is None and jastrow_data.jastrow_three_body_data is not None + # projection compilation. start_init = time.perf_counter() logger.info("Start compilation of the GFMC projection funciton.") @@ -1132,6 +1237,31 @@ def _update_inv_dn_t(_): self.__alat, self.__hamiltonian_data, ) + if use_streaming: + # Pre-compile the streaming variant on a fresh kinetic state so the + # while-loop inside the branching loop does not pay the JIT cost. + _init_kinetic_state_list_compile = vmap(_init_kinetic_energy_all_elements_streaming_state, in_axes=(None, 0, 0, 0))( + self.__hamiltonian_data.wavefunction_data, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + ) + (_, _, _, _, _, _, _, _, _, _) = vmap( + _projection_t_streaming, in_axes=(0, 0, 0, 0, 0, 0, 0, 0, None, None, None, None) + )( + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + self.__jax_PRNG_key_list, + _init_kinetic_state_list_compile, + self.__random_discretized_mesh, + self.__non_local_move, + self.__alat, + self.__hamiltonian_data, + ) end_init = time.perf_counter() timer_projection_init += end_init - start_init logger.info("End compilation of the GFMC projection funciton.") @@ -1435,31 +1565,72 @@ def _compute_local_energy_t( w_L_list = jnp.array([1.0 for _ in range(self.__num_walkers)], dtype=jnp.float64) start_projection = time.perf_counter() - # projection loop - while True: - ( - e_L_list, - projection_counter_list, - tau_left_list, - w_L_list, - self.__latest_r_up_carts, - self.__latest_r_dn_carts, - self.__latest_A_old_inv, - self.__jax_PRNG_key_list, - latest_RTs, - ) = vmap(_projection_t, in_axes=(0, 0, 0, 0, 0, 0, 0, None, None, None, None))( - projection_counter_list, - tau_left_list, - w_L_list, + # If streaming is enabled, build a fresh per-walker kinetic state + # at the start of each branching step (consistent with the freshly + # reset projection_counter / tau_left / w_L). The state is then + # advanced by ``_projection_t_streaming`` inside the while loop. + if use_streaming: + kinetic_state_list = vmap(_init_kinetic_energy_all_elements_streaming_state, in_axes=(None, 0, 0, 0))( + self.__hamiltonian_data.wavefunction_data, self.__latest_r_up_carts, self.__latest_r_dn_carts, self.__latest_A_old_inv, - self.__jax_PRNG_key_list, - self.__random_discretized_mesh, - self.__non_local_move, - self.__alat, - self.__hamiltonian_data, ) + # projection loop + while True: + if use_streaming: + ( + e_L_list, + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + kinetic_state_list, + self.__jax_PRNG_key_list, + latest_RTs, + ) = vmap( + _projection_t_streaming, + in_axes=(0, 0, 0, 0, 0, 0, 0, 0, None, None, None, None), + )( + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + self.__jax_PRNG_key_list, + kinetic_state_list, + self.__random_discretized_mesh, + self.__non_local_move, + self.__alat, + self.__hamiltonian_data, + ) + else: + ( + e_L_list, + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + self.__jax_PRNG_key_list, + latest_RTs, + ) = vmap(_projection_t, in_axes=(0, 0, 0, 0, 0, 0, 0, None, None, None, None))( + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + self.__jax_PRNG_key_list, + self.__random_discretized_mesh, + self.__non_local_move, + self.__alat, + self.__hamiltonian_data, + ) if np.max(tau_left_list) <= 0.0: break From 7a6e05a4903e20e01a4ccc8e16da850bb0a0585f Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Mon, 4 May 2026 00:46:19 +0900 Subject: [PATCH 38/97] Replace while loop with lax.while_loop in GFMC_t projection Replace the per-step Python `while True` in GFMC_t.run() with a `lax.while_loop` driver that captures the entire projection in one jit graph (one host sync per branching instead of one per step). The driver is defined once outside the branching loop so the jit cache hits after the first compile, and is AOT-compiled via `.lower(...).compile()` so branching step 0 no longer pays JIT cost. Fix: Also adds missing SWCT vmap warm-ups in both GFMC_t and GFMC_n precompilation blocks. --- jqmc/jqmc_gfmc.py | 264 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 208 insertions(+), 56 deletions(-) diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 1ed18e0a..430993c8 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -1507,6 +1507,13 @@ def _compute_local_energy_t( self.__latest_r_up_carts, self.__latest_r_dn_carts, ) + if self.__use_swct: + # Warm up SWCT vmap callables here so the very first branching + # step that consumes them does not pay JIT compile time. + _ = _jit_vmap_swct_omega_t(self.__hamiltonian_data.structure_data, self.__latest_r_up_carts) + _ = _jit_vmap_swct_omega_t(self.__hamiltonian_data.structure_data, self.__latest_r_dn_carts) + _ = _jit_vmap_swct_domega_t(self.__hamiltonian_data.structure_data, self.__latest_r_up_carts) + _ = _jit_vmap_swct_domega_t(self.__hamiltonian_data.structure_data, self.__latest_r_dn_carts) end_init_force = time.perf_counter() logger.info("End compilation of force gradient functions.") logger.info(f"Elapsed Time = {end_init_force - start_init_force:.2f} sec.") @@ -1515,6 +1522,138 @@ def _compute_local_energy_t( # Main branching loop. gfmc_interval = int(np.maximum(num_mcmc_steps / 100, 1)) # gfmc_projection set print-interval + # ------------------------------------------------------------------ + # Pre-build the per-branching projection driver. Defined ONCE here + # (outside the ``for i_branching`` loop) so that: + # * The Python closure objects ``_body_t`` / ``_body_t_streaming`` + # are stable across iterations -> the implicit jit cache key + # for ``lax.while_loop`` hits after the first compile. + # * Closing over ``self.__random_discretized_mesh``, + # ``self.__non_local_move``, ``self.__alat``, and + # ``self.__hamiltonian_data`` is safe because none of them + # change between branching steps within a single ``run()``. + # If these were defined inside the per-branching loop, every step + # would create a fresh function identity and trigger a full + # re-trace + re-compile (causing ~7 s/step on H100). + # ------------------------------------------------------------------ + def _cond_t(carry): + tau_left = carry[2] + return jnp.max(tau_left) > 0.0 + + _body_vmap_t = vmap( + _projection_t, + in_axes=(0, 0, 0, 0, 0, 0, 0, None, None, None, None), + ) + + def _body_t(carry): + (_, pcl, tll, wll, ru, rd, Ainv, key, _) = carry + return _body_vmap_t( + pcl, + tll, + wll, + ru, + rd, + Ainv, + key, + self.__random_discretized_mesh, + self.__non_local_move, + self.__alat, + self.__hamiltonian_data, + ) + + @jit + def _run_projection_loop(pcl, tll, wll, ru, rd, Ainv, key): + init_carry = _body_vmap_t( + pcl, + tll, + wll, + ru, + rd, + Ainv, + key, + self.__random_discretized_mesh, + self.__non_local_move, + self.__alat, + self.__hamiltonian_data, + ) + return lax.while_loop(_cond_t, _body_t, init_carry) + + if use_streaming: + _body_vmap_t_streaming = vmap( + _projection_t_streaming, + in_axes=(0, 0, 0, 0, 0, 0, 0, 0, None, None, None, None), + ) + + def _body_t_streaming(carry): + (_, pcl, tll, wll, ru, rd, Ainv, ks, key, _) = carry + return _body_vmap_t_streaming( + pcl, + tll, + wll, + ru, + rd, + Ainv, + key, + ks, + self.__random_discretized_mesh, + self.__non_local_move, + self.__alat, + self.__hamiltonian_data, + ) + + @jit + def _run_projection_loop_streaming(pcl, tll, wll, ru, rd, Ainv, key, ks): + init_carry = _body_vmap_t_streaming( + pcl, + tll, + wll, + ru, + rd, + Ainv, + key, + ks, + self.__random_discretized_mesh, + self.__non_local_move, + self.__alat, + self.__hamiltonian_data, + ) + return lax.while_loop(_cond_t, _body_t_streaming, init_carry) + + # ------------------------------------------------------------------ + # Warm up the lax.while_loop driver(s) via AOT compilation, so that + # the first ``branching step`` does NOT include compile time. + # ``.lower(*args).compile()`` traces & compiles without executing, + # so walker state / RNG keys are not consumed. + # ------------------------------------------------------------------ + start_warmup = time.perf_counter() + logger.info("Start compilation of the GFMC projection while_loop driver.") + logger.info(" Compilation is in progress...") + _run_projection_loop.lower( + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + self.__jax_PRNG_key_list, + ).compile() + if use_streaming: + _run_projection_loop_streaming.lower( + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + self.__jax_PRNG_key_list, + _init_kinetic_state_list_compile, + ).compile() + end_warmup = time.perf_counter() + timer_projection_init += end_warmup - start_warmup + logger.info("End compilation of the GFMC projection while_loop driver.") + logger.info(f"Elapsed Time = {end_warmup - start_warmup:.2f} sec.") + logger.info("") + logger.info("-Start branching-") progress = (self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 gmfc_total_current = time.perf_counter() @@ -1577,62 +1716,68 @@ def _compute_local_energy_t( self.__latest_A_old_inv, ) # projection loop - while True: - if use_streaming: - ( - e_L_list, - projection_counter_list, - tau_left_list, - w_L_list, - self.__latest_r_up_carts, - self.__latest_r_dn_carts, - self.__latest_A_old_inv, - kinetic_state_list, - self.__jax_PRNG_key_list, - latest_RTs, - ) = vmap( - _projection_t_streaming, - in_axes=(0, 0, 0, 0, 0, 0, 0, 0, None, None, None, None), - )( - projection_counter_list, - tau_left_list, - w_L_list, - self.__latest_r_up_carts, - self.__latest_r_dn_carts, - self.__latest_A_old_inv, - self.__jax_PRNG_key_list, - kinetic_state_list, - self.__random_discretized_mesh, - self.__non_local_move, - self.__alat, - self.__hamiltonian_data, - ) - else: - ( - e_L_list, - projection_counter_list, - tau_left_list, - w_L_list, - self.__latest_r_up_carts, - self.__latest_r_dn_carts, - self.__latest_A_old_inv, - self.__jax_PRNG_key_list, - latest_RTs, - ) = vmap(_projection_t, in_axes=(0, 0, 0, 0, 0, 0, 0, None, None, None, None))( - projection_counter_list, - tau_left_list, - w_L_list, - self.__latest_r_up_carts, - self.__latest_r_dn_carts, - self.__latest_A_old_inv, - self.__jax_PRNG_key_list, - self.__random_discretized_mesh, - self.__non_local_move, - self.__alat, - self.__hamiltonian_data, - ) - if np.max(tau_left_list) <= 0.0: - break + # + # The previous implementation was a Python ``while True`` that + # dispatched ``vmap(_projection_t)`` from the host once per + # projection step and broke on ``np.max(tau_left_list) <= 0``. + # That ``np.max`` forces a host-side jax->numpy materialization, + # which blocks on the GPU once per step (so 27 projections => + # 27 host syncs and 27 jit dispatches per branching). On H100 + # this dominates wall time for small systems (e.g. 01water: + # ~4.5 ms/step vs <0.5 ms/step measured for GFMC_n which uses + # ``lax.fori_loop``). We replace it with ``lax.while_loop`` so + # the entire projection loop is captured into a single jit graph + # (CUDA-graph friendly) and only one host sync happens at the + # end via ``block_until_ready`` below. The cond is evaluated on + # device (``jnp.max(tau_left) > 0.0``). + # + # The driver functions ``_run_projection_loop_*`` are defined + # **outside** this ``for i_branching`` loop (see above) so the + # jit cache hits after the first compile; defining them here + # would create a fresh Python closure each branching and force + # a re-trace + re-compile per step (catastrophic slowdown). + if use_streaming: + ( + e_L_list, + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + kinetic_state_list, + self.__jax_PRNG_key_list, + latest_RTs, + ) = _run_projection_loop_streaming( + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + self.__jax_PRNG_key_list, + kinetic_state_list, + ) + else: + ( + e_L_list, + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + self.__jax_PRNG_key_list, + latest_RTs, + ) = _run_projection_loop( + projection_counter_list, + tau_left_list, + w_L_list, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + self.__latest_A_old_inv, + self.__jax_PRNG_key_list, + ) # sync. jax arrays computations. e_L_list.block_until_ready() @@ -5579,6 +5724,13 @@ def _compute_local_energy_n( self.__latest_r_up_carts, self.__latest_r_dn_carts, ) + if self.__use_swct: + # Warm up SWCT vmap callables so they are not JIT-compiled + # inside the main MCMC loop. + _ = _jit_vmap_swct_omega_n(self.__hamiltonian_data.structure_data, self.__latest_r_up_carts) + _ = _jit_vmap_swct_omega_n(self.__hamiltonian_data.structure_data, self.__latest_r_dn_carts) + _ = _jit_vmap_swct_domega_n(self.__hamiltonian_data.structure_data, self.__latest_r_up_carts) + _ = _jit_vmap_swct_domega_n(self.__hamiltonian_data.structure_data, self.__latest_r_dn_carts) end_init = time.perf_counter() timer_projection_init += end_init - start_init logger.info("End compilation of the GFMC projection funciton.") From 7aad0d06de102fc66fcaafa92d7c9b6ee27248bb Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Mon, 4 May 2026 11:23:40 +0900 Subject: [PATCH 39/97] Update test sets. --- .github/workflows/jqmc-run-full-pytest.yml | 41 +++++++-- .github/workflows/jqmc-run-long-pytest.yml | 96 +++++++++++++++++++++ .github/workflows/jqmc-run-rc-pytest.yml | 11 --- .github/workflows/jqmc-run-short-pytest.yml | 19 +++- 4 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/jqmc-run-long-pytest.yml diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index cee32e6c..0739fa65 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -3,7 +3,7 @@ name: jqmc full test on: - pull_request: + push: branches: [ "main" ] paths-ignore: - '.gitignore' @@ -54,11 +54,11 @@ jobs: # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test jqmc (intra-software comparisons) + - name: Test jqmc FP64 (intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail - pytest -s -v tests/test_init_electron_configurations.py --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_structure.py --cov-branch --no-cov-on-fail --cov-append + pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -s -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append @@ -72,14 +72,41 @@ jobs: pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_mixed_precision.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - name: Test jqmc (inter-software comparisons) + - name: Test jqmc FP32+FP64 (intra-software comparisons) + run: | + pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + + - name: Test jqmc FP64 (inter-software comparisons) run: | pytest -s -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - name: Test jqmc (QMC kernels without MPI) + - name: Test jqmc FP32+FP64 (QMC kernels without MPI) + run: | + pytest -s -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + + - name: Test jqmc FP64 (QMC kernels without MPI) run: | pytest -s -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append diff --git a/.github/workflows/jqmc-run-long-pytest.yml b/.github/workflows/jqmc-run-long-pytest.yml new file mode 100644 index 00000000..e526f38a --- /dev/null +++ b/.github/workflows/jqmc-run-long-pytest.yml @@ -0,0 +1,96 @@ +# A long test of jqmc. + +name: jqmc long test + +on: + pull_request: + branches: [ "main" ] + paths-ignore: + - '.gitignore' + - '.github/**' + - 'doc/**' + - 'examples/**' + - 'benchmarks/**' + - 'README.md' + - '.pre-commit-config.yaml' + - 'jqmc_workflow/**' + +jobs: + run: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - name: Install gfortran and gcc + run: | + sudo apt-get update + sudo apt-get install gfortran + + - name: Install OpenBLAS and LAPACK + run: sudo apt-get install libopenblas-dev liblapack-dev + + - name: Install OpenMPI + run: sudo apt-get install openmpi-bin libopenmpi-dev + + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install jqmc + run: | + python -m pip install flake8 pytest pytest-cov + python -m pip install . + + #- name: Lint jqmc with flake8 + # run: | + # # stop the build if there are Python syntax errors or undefined names + # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Test jqmc FP64/FP32+FP64 (intra-software comparisons) + run: | + pytest -s -v tests/test_trexio.py + pytest -s -v tests/test_init_electron_configurations.py + pytest -s -v tests/test_structure.py + pytest -s -v tests/test_AOs.py + pytest -s -v tests/test_AOs.py --precision-mode=mixed + pytest -s -v tests/test_MOs.py + pytest -s -v tests/test_determinant.py + pytest -s -v tests/test_jastrow.py + pytest -s -v tests/test_wave_function.py + pytest -s -v tests/test_wave_function.py --precision-mode=mixed + pytest -s -v tests/test_ecps.py + pytest -s -v tests/test_swct.py + pytest -s -v tests/test_mcmc_force.py + pytest -s -v tests/test_lrdmc_force.py + pytest -s -v tests/test_mcmc_force.py --precision-mode=mixed + pytest -s -v tests/test_lrdmc_force.py --precision-mode=mixed + pytest -s -v tests/test_checkpoint_components.py + pytest -s -v tests/test_checkpoint_mcmc.py + pytest -s -v tests/test_checkpoint_gfmc.py + pytest -s -v tests/test_ao_basis_optimization.py + pytest -s -v tests/test_mixed_precision.py --precision-mode=mixed + + - name: Test jqmc FP64 (inter-software comparisons) + run: | + pytest -s -v tests/test_comparison_with_turborvb_ECP.py + pytest -s -v tests/test_comparison_with_turborvb_AE.py + + - name: Test jqmc FP64/FP32+FP64 (QMC kernels without MPI) + run: | + pytest -s -v tests/test_jqmc_command_lines.py + pytest -s -v tests/test_jqmc_mcmc.py + pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed + pytest -s -v tests/test_jqmc_gfmc_tau.py + pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + + - name: Test jqmc-tool (toolset for jqmc) + run: | + pytest -s -v tests/test_jqmc_tool.py diff --git a/.github/workflows/jqmc-run-rc-pytest.yml b/.github/workflows/jqmc-run-rc-pytest.yml index 76785a10..95f8b433 100644 --- a/.github/workflows/jqmc-run-rc-pytest.yml +++ b/.github/workflows/jqmc-run-rc-pytest.yml @@ -3,17 +3,6 @@ name: jqmc rc test on: - push: - branches: [ "rc" ] - paths-ignore: - - '.gitignore' - - '.github/**' - - 'doc/**' - - 'examples/**' - - 'benchmarks/**' - - 'README.md' - - '.pre-commit-config.yaml' - - 'jqmc_workflow/**' pull_request: branches: [ "rc" ] paths-ignore: diff --git a/.github/workflows/jqmc-run-short-pytest.yml b/.github/workflows/jqmc-run-short-pytest.yml index e36ce68f..004bb96b 100644 --- a/.github/workflows/jqmc-run-short-pytest.yml +++ b/.github/workflows/jqmc-run-short-pytest.yml @@ -65,7 +65,7 @@ jobs: # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test jqmc (pytest) with @jit decorator (intra-software comparisons) + - name: Test jqmc FP64 (intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --skip-heavy pytest -s -v tests/test_init_electron_configurations.py --skip-heavy @@ -81,7 +81,18 @@ jobs: pytest -s -v tests/test_lrdmc_force.py --skip-heavy pytest -s -v tests/test_mixed_precision.py --precision-mode=mixed --skip-heavy - - name: Test jqmc (pytest) with @jit decorator (inter-software comparisons) + - name: Test jqmc FP32+FP64 (intra-software comparisons) run: | - pytest -s -v tests/test_comparison_with_turborvb_ECP.py --skip-heavy - pytest -s -v tests/test_comparison_with_turborvb_AE.py --skip-heavy + pytest -s -v tests/test_trexio.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_init_electron_configurations.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_structure.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_AOs.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_MOs.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_determinant.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_jastrow.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_wave_function.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_ecps.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_swct.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_mcmc_force.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_lrdmc_force.py --skip-heavy --precision-mode=mixed + pytest -s -v tests/test_mixed_precision.py --skip-heavy --precision-mode=mixed From 4db0829e98bbffdb85af20a43dbc350bffabcced Mon Sep 17 00:00:00 2001 From: kousuke Date: Mon, 4 May 2026 14:57:36 +0900 Subject: [PATCH 40/97] Fix markers and tols in tests/test_hamiltonian.py and tests/test_wave_function.py --- tests/test_hamiltonian.py | 7 +++++- tests/test_wave_function.py | 49 +++++++++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/tests/test_hamiltonian.py b/tests/test_hamiltonian.py index c76c1254..6ed0b614 100644 --- a/tests/test_hamiltonian.py +++ b/tests/test_hamiltonian.py @@ -215,7 +215,12 @@ def test_compute_local_energy_fast(trexio_file): n_up = geminal_data.num_electron_up n_dn = geminal_data.num_electron_dn - atol, rtol = get_tolerance("local_energy", "strict") + # e_L crosses ao_eval/jastrow_eval/det_eval/coulomb/wf_kinetic/local_energy + # zones; the achievable agreement is bounded by the weakest (fp32 in mixed). + atol, rtol = get_tolerance_min( + ("ao_eval", "jastrow_eval", "det_eval", "coulomb", "wf_kinetic", "local_energy"), + "strict", + ) for _ in range(10): r_up = jnp.array(first_nucleus + rng.standard_normal((n_up, 3)) * 1.2, dtype=jnp.float64) r_dn = jnp.array(first_nucleus + rng.standard_normal((n_dn, 3)) * 1.2, dtype=jnp.float64) diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index 2e270d43..97c6f260 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -175,16 +175,29 @@ def test_kinetic_energy_analytic_and_auto(trexio_file: str): r_dn_carts=jnp.asarray(r_dn_carts), ) - atol, rtol = get_tolerance("wf_kinetic", "strict") + # T_L crosses ao_eval/jastrow_eval/jastrow_grad_lap/wf_kinetic zones; the + # achievable analytic-vs-auto agreement is bounded by the weakest (fp32 in mixed). + atol, rtol = get_tolerance_min( + ("ao_eval", "jastrow_eval", "jastrow_grad_lap", "wf_kinetic"), + "strict", + ) assert not np.any(np.isnan(np.asarray(K_analytic))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_auto))), "NaN detected in second argument" np.testing.assert_allclose(K_analytic, K_auto, atol=atol, rtol=rtol) @pytest.mark.activate_if_skip_heavy +@pytest.mark.numerical_diff @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) def test_debug_and_auto_kinetic_energy_all_elements(trexio_file: str): - """Debug vs autodiff kinetic energy per-electron arrays.""" + """Debug vs autodiff kinetic energy per-electron arrays. + + The debug path computes ``-1/2 · ∇²Psi / Psi`` via central finite differences + on Psi (h = 2e-4); under mixed precision the fp32 round-off in ao_eval / + jastrow_eval propagates into Psi at ~1e-7 and is amplified by 1/h² = 2.5e7, + giving an O(1) relative error in the FD Laplacian. Marked ``numerical_diff`` + so conftest skips it under ``--precision-mode=mixed``. + """ ( _, aos_data, @@ -294,7 +307,12 @@ def test_auto_and_analytic_kinetic_energy_all_elements(trexio_file: str): wavefunction_data=wavefunction_data, r_up_carts=r_up_carts_jnp, r_dn_carts=r_dn_carts_jnp ) - atol, rtol = get_tolerance("wf_kinetic", "strict") + # T_L crosses ao_eval/jastrow_eval/jastrow_grad_lap/wf_kinetic zones; the + # achievable analytic-vs-auto agreement is bounded by the weakest (fp32 in mixed). + atol, rtol = get_tolerance_min( + ("ao_eval", "jastrow_eval", "jastrow_grad_lap", "wf_kinetic"), + "strict", + ) assert not np.any(np.isnan(np.asarray(K_elements_up_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_up_analytic))), "NaN detected in second argument" np.testing.assert_allclose(K_elements_up_auto, K_elements_up_analytic, atol=atol, rtol=rtol) @@ -362,7 +380,13 @@ def test_fast_update_kinetic_energy_all_elements(trexio_file: str): geminal_inverse=A_inv, ) - atol, rtol = get_tolerance("wf_kinetic", "strict") + # Fast-update path crosses ao_eval/jastrow_eval/jastrow_grad_lap/det_ratio/ + # wf_kinetic zones; the achievable agreement is bounded by the weakest + # (fp32 in mixed for ao_eval / jastrow_eval / jastrow_grad_lap). + atol, rtol = get_tolerance_min( + ("ao_eval", "jastrow_eval", "jastrow_grad_lap", "det_ratio", "wf_kinetic"), + "strict", + ) assert not np.any(np.isnan(np.asarray(ke_up_fast))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(ke_up_debug))), "NaN detected in second argument" np.testing.assert_allclose(ke_up_fast, ke_up_debug, atol=atol, rtol=rtol) @@ -444,7 +468,22 @@ def test_debug_and_jax_discretized_kinetic_energy(trexio_file: str): RT=RT, ) - atol, rtol = get_tolerance("wf_kinetic", "strict") + # Discretized kinetic energy (LRDMC) crosses ao_eval/jastrow_eval/ + # jastrow_grad_lap/jastrow_ratio/det_ratio/wf_ratio/wf_kinetic zones; the + # fast_update path uses Sherman-Morrison ratios. Agreement is bounded by + # the weakest (fp32 in mixed for ao_eval / jastrow_* zones). + atol, rtol = get_tolerance_min( + ( + "ao_eval", + "jastrow_eval", + "jastrow_grad_lap", + "jastrow_ratio", + "det_ratio", + "wf_ratio", + "wf_kinetic", + ), + "strict", + ) assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_up_carts_jax))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mesh_kinetic_part_r_up_carts_debug))), "NaN detected in second argument" np.testing.assert_allclose( From b6dc21c67528134a67e319fc627036444404204a Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Mon, 4 May 2026 18:22:50 +0900 Subject: [PATCH 41/97] Make the numerical laplacian debug functions more stable. --- jqmc/determinant.py | 175 ++++++------------------------------ jqmc/wavefunction.py | 52 ++++++----- tests/test_determinant.py | 104 +++++++-------------- tests/test_wave_function.py | 14 ++- 4 files changed, 105 insertions(+), 240 deletions(-) diff --git a/jqmc/determinant.py b/jqmc/determinant.py index e20501f1..e25911f0 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -2829,94 +2829,44 @@ def _compute_grads_and_laplacian_ln_Det_debug( grad_ln_D_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn]).T / det_geminal ############################################################# - # Laplacian part + # Laplacian part (4th-order central finite differences) + # f''(x) ≈ (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h²) ############################################################# - diff_h2 = 1.0e-4 # for laplacian + diff_h2 = 1.0e-3 # larger h viable with 4th-order stencil (O(h⁴) truncation) laplacian_ln_D_up = np.zeros(len(r_up_carts)) laplacian_ln_D_dn = np.zeros(len(r_dn_carts)) - # laplacians up - for r_i, _ in enumerate(r_up_carts): - diff_p_x_r_up2_carts = r_up_carts.copy() - diff_p_y_r_up2_carts = r_up_carts.copy() - diff_p_z_r_up2_carts = r_up_carts.copy() - diff_p_x_r_up2_carts[r_i][0] += diff_h2 - diff_p_y_r_up2_carts[r_i][1] += diff_h2 - diff_p_z_r_up2_carts[r_i][2] += diff_h2 + def _det_up(r_up): + return compute_det_geminal_all_elements(geminal_data=geminal_data, r_up_carts=r_up, r_dn_carts=r_dn_carts) - det_geminal_p_x_up2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=diff_p_x_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - det_geminal_p_y_up2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=diff_p_y_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - det_geminal_p_z_up2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=diff_p_z_r_up2_carts, - r_dn_carts=r_dn_carts, - ) + def _det_dn(r_dn): + return compute_det_geminal_all_elements(geminal_data=geminal_data, r_up_carts=r_up_carts, r_dn_carts=r_dn) - diff_m_x_r_up2_carts = r_up_carts.copy() - diff_m_y_r_up2_carts = r_up_carts.copy() - diff_m_z_r_up2_carts = r_up_carts.copy() - diff_m_x_r_up2_carts[r_i][0] -= diff_h2 - diff_m_y_r_up2_carts[r_i][1] -= diff_h2 - diff_m_z_r_up2_carts[r_i][2] -= diff_h2 + def _fd4_second_deriv(eval_fn, r_carts, r_i, dim, h, f0): + """4th-order central FD for f''(x).""" + r_p1 = r_carts.copy() + r_p2 = r_carts.copy() + r_m1 = r_carts.copy() + r_m2 = r_carts.copy() + r_p1[r_i][dim] += h + r_p2[r_i][dim] += 2 * h + r_m1[r_i][dim] -= h + r_m2[r_i][dim] -= 2 * h + return (-eval_fn(r_p2) + 16 * eval_fn(r_p1) - 30 * f0 + 16 * eval_fn(r_m1) - eval_fn(r_m2)) / (12 * h**2) - det_geminal_m_x_up2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=diff_m_x_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - det_geminal_m_y_up2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=diff_m_y_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - det_geminal_m_z_up2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=diff_m_z_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - - """ mathematically correct, but numerically unstablle - gradgrad_x_up = ( - np.log(np.abs(det_geminal_p_x_up2)) - + np.log(np.abs(det_geminal_m_x_up2)) - - 2.0 * np.log(np.abs(det_geminal)) - ) / (diff_h2**2) - - gradgrad_y_up = ( - np.log(np.abs(det_geminal_p_y_up2)) - + np.log(np.abs(det_geminal_m_y_up2)) - - 2.0 * np.log(np.abs(det_geminal)) - ) / (diff_h2**2) - - gradgrad_z_up = ( - np.log(np.abs(det_geminal_p_z_up2)) - + np.log(np.abs(det_geminal_m_z_up2)) - - 2.0 * np.log(np.abs(det_geminal)) - ) / (diff_h2**2) - """ - - # compute f''(x) - gradgrad_x_up = (det_geminal_p_x_up2 + det_geminal_m_x_up2 - 2.0 * det_geminal) / (diff_h2**2) - - gradgrad_y_up = (det_geminal_p_y_up2 + det_geminal_m_y_up2 - 2.0 * det_geminal) / (diff_h2**2) - - gradgrad_z_up = (det_geminal_p_z_up2 + det_geminal_m_z_up2 - 2.0 * det_geminal) / (diff_h2**2) + # laplacians up + for r_i, _ in enumerate(r_up_carts): + gradgrad_x_up = _fd4_second_deriv(_det_up, r_up_carts, r_i, 0, diff_h2, det_geminal) + gradgrad_y_up = _fd4_second_deriv(_det_up, r_up_carts, r_i, 1, diff_h2, det_geminal) + gradgrad_z_up = _fd4_second_deriv(_det_up, r_up_carts, r_i, 2, diff_h2, det_geminal) _grad_x_up = grad_x_up[r_i] _grad_y_up = grad_y_up[r_i] _grad_z_up = grad_z_up[r_i] - # since d^2/dx^2 ln(|f(x)|) = (f''(x)*f(x) - f'(x)^2) / f(x)^2 + # d^2/dx^2 ln(|f(x)|) = (f''(x)*f(x) - f'(x)^2) / f(x)^2 laplacian_ln_D_up[r_i] = ( (gradgrad_x_up * det_geminal - _grad_x_up**2) / det_geminal**2 + (gradgrad_y_up * det_geminal - _grad_y_up**2) / det_geminal**2 @@ -2925,84 +2875,15 @@ def _compute_grads_and_laplacian_ln_Det_debug( # laplacians dn for r_i, _ in enumerate(r_dn_carts): - diff_p_x_r_dn2_carts = r_dn_carts.copy() - diff_p_y_r_dn2_carts = r_dn_carts.copy() - diff_p_z_r_dn2_carts = r_dn_carts.copy() - diff_p_x_r_dn2_carts[r_i][0] += diff_h2 - diff_p_y_r_dn2_carts[r_i][1] += diff_h2 - diff_p_z_r_dn2_carts[r_i][2] += diff_h2 - - det_geminal_p_x_dn2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_p_x_r_dn2_carts, - ) - det_geminal_p_y_dn2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_p_y_r_dn2_carts, - ) - det_geminal_p_z_dn2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_p_z_r_dn2_carts, - ) - - diff_m_x_r_dn2_carts = r_dn_carts.copy() - diff_m_y_r_dn2_carts = r_dn_carts.copy() - diff_m_z_r_dn2_carts = r_dn_carts.copy() - diff_m_x_r_dn2_carts[r_i][0] -= diff_h2 - diff_m_y_r_dn2_carts[r_i][1] -= diff_h2 - diff_m_z_r_dn2_carts[r_i][2] -= diff_h2 - - det_geminal_m_x_dn2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_m_x_r_dn2_carts, - ) - det_geminal_m_y_dn2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_m_y_r_dn2_carts, - ) - det_geminal_m_z_dn2 = compute_det_geminal_all_elements( - geminal_data=geminal_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_m_z_r_dn2_carts, - ) - - """ mathematically correct, but numerically unstable - gradgrad_x_dn = ( - np.log(np.abs(det_geminal_p_x_dn2)) - + np.log(np.abs(det_geminal_m_x_dn2)) - - 2.0 * np.log(np.abs(det_geminal)) - ) / (diff_h2**2) - - gradgrad_y_dn = ( - np.log(np.abs(det_geminal_p_y_dn2)) - + np.log(np.abs(det_geminal_m_y_dn2)) - - 2.0 * np.log(np.abs(det_geminal)) - ) / (diff_h2**2) - - gradgrad_z_dn = ( - np.log(np.abs(det_geminal_p_z_dn2)) - + np.log(np.abs(det_geminal_m_z_dn2)) - - 2.0 * np.log(np.abs(det_geminal)) - ) / (diff_h2**2) - """ - - # compute f''(x) - gradgrad_x_dn = (det_geminal_p_x_dn2 + det_geminal_m_x_dn2 - 2.0 * det_geminal) / (diff_h2**2) - - gradgrad_y_dn = (det_geminal_p_y_dn2 + det_geminal_m_y_dn2 - 2.0 * det_geminal) / (diff_h2**2) - - gradgrad_z_dn = (det_geminal_p_z_dn2 + det_geminal_m_z_dn2 - 2.0 * det_geminal) / (diff_h2**2) + gradgrad_x_dn = _fd4_second_deriv(_det_dn, r_dn_carts, r_i, 0, diff_h2, det_geminal) + gradgrad_y_dn = _fd4_second_deriv(_det_dn, r_dn_carts, r_i, 1, diff_h2, det_geminal) + gradgrad_z_dn = _fd4_second_deriv(_det_dn, r_dn_carts, r_i, 2, diff_h2, det_geminal) _grad_x_dn = grad_x_dn[r_i] _grad_y_dn = grad_y_dn[r_i] _grad_z_dn = grad_z_dn[r_i] - # since d^2/dx^2 ln(|f(x)|) = (f''(x)*f(x) - f'(x)^2) / f(x)^2 + # d^2/dx^2 ln(|f(x)|) = (f''(x)*f(x) - f'(x)^2) / f(x)^2 laplacian_ln_D_dn[r_i] = ( (gradgrad_x_dn * det_geminal - _grad_x_dn**2) / det_geminal**2 + (gradgrad_y_dn * det_geminal - _grad_y_dn**2) / det_geminal**2 diff --git a/jqmc/wavefunction.py b/jqmc/wavefunction.py index 4fd055da..2c5bcd56 100644 --- a/jqmc/wavefunction.py +++ b/jqmc/wavefunction.py @@ -986,39 +986,49 @@ def _compute_kinetic_energy_all_elements_debug( r_up_carts: npt.NDArray[np.float64], r_dn_carts: npt.NDArray[np.float64], ) -> float | complex: - """See compute_kinetic_energy_api.""" - # compute laplacians - diff_h = 2.0e-4 + """See compute_kinetic_energy_api. + + Uses 4th-order central finite differences for the Laplacian: + f''(x) ≈ (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h²) + This allows a larger step size h while maintaining accuracy (O(h⁴) truncation error). + """ + diff_h = 1.0e-3 # larger h is viable with 4th-order stencil Psi = evaluate_wavefunction(wavefunction_data, r_up_carts, r_dn_carts) + def _eval_up(r_up): + return evaluate_wavefunction(wavefunction_data, r_up, r_dn_carts) + + def _eval_dn(r_dn): + return evaluate_wavefunction(wavefunction_data, r_up_carts, r_dn) + + def _fd4_second_deriv(eval_fn, r_carts, i, d, h): + """4th-order central FD for d²f/dx².""" + r_p1 = r_carts.copy() + r_p2 = r_carts.copy() + r_m1 = r_carts.copy() + r_m2 = r_carts.copy() + r_p1[i, d] += h + r_p2[i, d] += 2 * h + r_m1[i, d] -= h + r_m2[i, d] -= 2 * h + f_p1 = eval_fn(r_p1) + f_p2 = eval_fn(r_p2) + f_m1 = eval_fn(r_m1) + f_m2 = eval_fn(r_m2) + return (-f_p2 + 16 * f_p1 - 30 * Psi + 16 * f_m1 - f_m2) / (12 * h**2) + n_up, d_up = r_up_carts.shape laplacian_Psi_up = np.zeros(n_up) for i in range(n_up): for d in range(d_up): - r_up_plus = r_up_carts.copy() - r_up_minus = r_up_carts.copy() - r_up_plus[i, d] += diff_h - r_up_minus[i, d] -= diff_h - - Psi_plus = evaluate_wavefunction(wavefunction_data, r_up_plus, r_dn_carts) - Psi_minus = evaluate_wavefunction(wavefunction_data, r_up_minus, r_dn_carts) - - laplacian_Psi_up[i] += (Psi_plus + Psi_minus - 2 * Psi) / (diff_h**2) + laplacian_Psi_up[i] += _fd4_second_deriv(_eval_up, r_up_carts, i, d, diff_h) n_dn, d_dn = r_dn_carts.shape laplacian_Psi_dn = np.zeros(n_dn) for i in range(n_dn): for d in range(d_dn): - r_dn_plus = r_dn_carts.copy() - r_dn_minus = r_dn_carts.copy() - r_dn_plus[i, d] += diff_h - r_dn_minus[i, d] -= diff_h - - Psi_plus = evaluate_wavefunction(wavefunction_data, r_up_carts, r_dn_plus) - Psi_minus = evaluate_wavefunction(wavefunction_data, r_up_carts, r_dn_minus) - - laplacian_Psi_dn[i] += (Psi_plus + Psi_minus - 2 * Psi) / (diff_h**2) + laplacian_Psi_dn[i] += _fd4_second_deriv(_eval_dn, r_dn_carts, i, d, diff_h) kinetic_energy_all_elements_up = -1.0 / 2.0 * laplacian_Psi_up / Psi kinetic_energy_all_elements_dn = -1.0 / 2.0 * laplacian_Psi_dn / Psi diff --git a/tests/test_determinant.py b/tests/test_determinant.py index f061d100..fe2cae9e 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -1077,12 +1077,6 @@ def test_numerial_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): num_electron_up = geminal_mo_data.num_electron_up num_electron_dn = geminal_mo_data.num_electron_dn - # Initialization - r_up_carts = [] - r_dn_carts = [] - - total_electrons = 0 - if coulomb_potential_data.ecp_flag: charges = np.array(structure_data.atomic_numbers) - np.array(coulomb_potential_data.z_cores) else: @@ -1090,73 +1084,43 @@ def test_numerial_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): coords = structure_data._positions_cart_np - # Place electrons around each nucleus - for i in range(len(coords)): - charge = charges[i] - num_electrons = int(np.round(charge)) # Number of electrons to place based on the charge - - # Retrieve the position coordinates - x, y, z = coords[i] - - # Place electrons - for _ in range(num_electrons): - # Calculate distance range - distance = np.random.uniform(0.5 / charge, 1.5 / charge) - theta = np.random.uniform(0, np.pi) - phi = np.random.uniform(0, 2 * np.pi) - - # Convert spherical to Cartesian coordinates - dx = distance * np.sin(theta) * np.cos(phi) - dy = distance * np.sin(theta) * np.sin(phi) - dz = distance * np.cos(theta) - - # Position of the electron - electron_position = np.array([x + dx, y + dy, z + dz]) - - # Assign spin - if len(r_up_carts) < num_electron_up: - r_up_carts.append(electron_position) - else: - r_dn_carts.append(electron_position) - - total_electrons += num_electrons - - # Handle surplus electrons - remaining_up = num_electron_up - len(r_up_carts) - remaining_dn = num_electron_dn - len(r_dn_carts) - - # Randomly place any remaining electrons - for _ in range(remaining_up): - r_up_carts.append(np.random.choice(coords) + np.random.normal(scale=0.1, size=3)) - for _ in range(remaining_dn): - r_dn_carts.append(np.random.choice(coords) + np.random.normal(scale=0.1, size=3)) - - r_up_carts = np.array(r_up_carts).reshape(-1, 3) - r_dn_carts = np.array(r_dn_carts).reshape(-1, 3) - - """ - mo_lambda_matrix_paired, mo_lambda_matrix_unpaired = np.hsplit(geminal_mo_data.lambda_matrix, [geminal_mo_data.orb_num_dn]) - - # generate matrices for the test - ao_lambda_matrix_paired = np.dot( - mos_data_up.mo_coefficients.T, - np.dot(mo_lambda_matrix_paired, mos_data_dn.mo_coefficients), - ) - ao_lambda_matrix_unpaired = np.dot(mos_data_up.mo_coefficients.T, mo_lambda_matrix_unpaired) - ao_lambda_matrix = np.hstack([ao_lambda_matrix_paired, ao_lambda_matrix_unpaired]) - - geminal_ao_data = Geminal_data( - num_electron_up=num_electron_up, - num_electron_dn=num_electron_dn, - orb_data_up_spin=aos_data, - orb_data_dn_spin=aos_data, - lambda_matrix=ao_lambda_matrix, - ) - """ - geminal_ao_data = Geminal_data.convert_from_MOs_to_AOs(geminal_mo_data) geminal_ao_data.sanity_check() + # Generate electron configuration far from determinant nodes so that + # numerical 2nd derivatives are well-conditioned. + def _generate_config(): + r_up = [] + r_dn = [] + for i in range(len(coords)): + charge = charges[i] + num_electrons = int(np.round(charge)) + x, y, z = coords[i] + for _ in range(num_electrons): + distance = np.random.uniform(0.5 / max(charge, 1), 1.5 / max(charge, 1)) + theta = np.random.uniform(0, np.pi) + phi = np.random.uniform(0, 2 * np.pi) + dx = distance * np.sin(theta) * np.cos(phi) + dy = distance * np.sin(theta) * np.sin(phi) + dz = distance * np.cos(theta) + if len(r_up) < num_electron_up: + r_up.append(np.array([x + dx, y + dy, z + dz])) + else: + r_dn.append(np.array([x + dx, y + dy, z + dz])) + for _ in range(num_electron_up - len(r_up)): + r_up.append(np.random.choice(coords) + np.random.normal(scale=0.2, size=3)) + for _ in range(num_electron_dn - len(r_dn)): + r_dn.append(np.random.choice(coords) + np.random.normal(scale=0.2, size=3)) + return np.array(r_up).reshape(-1, 3), np.array(r_dn).reshape(-1, 3) + + for _ in range(500): + r_up_carts, r_dn_carts = _generate_config() + det_val = compute_det_geminal_all_elements(geminal_data=geminal_ao_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) + if abs(det_val) > 1e-8: + break + else: + pytest.skip("Could not find electron configuration sufficiently far from determinant node") + grad_ln_D_up_numerical, grad_ln_D_dn_numerical, lap_ln_D_up_numerical, lap_ln_D_dn_numerical = ( _compute_grads_and_laplacian_ln_Det_debug( geminal_data=geminal_ao_data, diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index 97c6f260..f7c51de3 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -119,8 +119,18 @@ def test_kinetic_energy_analytic_and_numerical(trexio_file: str): num_ele_up = geminal_mo_data.num_electron_up num_ele_dn = geminal_mo_data.num_electron_dn r_cart_min, r_cart_max = -2.0, +2.0 - r_up_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_up, 3) + r_cart_min - r_dn_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_dn, 3) + r_cart_min + # Generate electron configuration away from wavefunction nodes to ensure + # numerical 2nd derivatives are well-conditioned (|Psi| >> 0). + from jqmc.wavefunction import evaluate_wavefunction + + for _ in range(200): + r_up_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_up, 3) + r_cart_min + r_dn_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_dn, 3) + r_cart_min + psi_val = evaluate_wavefunction(wavefunction_data, r_up_carts, r_dn_carts) + if abs(psi_val) > 1e-8: + break + else: + pytest.skip("Could not find electron configuration sufficiently far from node") K_debug = _compute_kinetic_energy_debug(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) K_jax = compute_kinetic_energy(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) From fadfa99382d81e342f553eddc0c0a70165456822 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Mon, 4 May 2026 23:56:50 +0900 Subject: [PATCH 42/97] Fix det grad/lap fused-vs-standalone AO mismatch + clean up docstrings Switch `compute_grads_and_laplacian_ln_Det` to the fused `compute_orb_value_grad_lap_api` so all three determinant grad/lap paths (`standard`, `_fast`, and `_grads_lap_body`) share bit-identical AO outputs. This fixes the standalone-vs-fused fp32 round-off divergence that broke `test_fast_update_kinetic_energy_all_elements` on `N_ae_ccpvdz_cart` in mixed mode. --- jqmc/_precision.py | 78 ++++++++++++++++----------------------- jqmc/atomic_orbital.py | 50 ++++++++----------------- jqmc/determinant.py | 23 ++++++------ jqmc/jastrow_factor.py | 6 +-- jqmc/jqmc_gfmc.py | 11 ++---- jqmc/molecular_orbital.py | 2 +- 6 files changed, 67 insertions(+), 103 deletions(-) diff --git a/jqmc/_precision.py b/jqmc/_precision.py index 7cd3a113..edf28d20 100644 --- a/jqmc/_precision.py +++ b/jqmc/_precision.py @@ -195,29 +195,24 @@ def compute_coulomb(r_carts, R_carts): \\* ``mo_eval`` is a high-risk zone even though the consumed AO values are fp32: the small ``mo_coefficients @ aos`` matmul is run in this zone, and its output feeds the determinant matrix, where fp32 round-off is -amplified by log|det|. See ``bug/fp32`` diagnostics. +amplified by log|det|. † ``jastrow_eval`` and ``wf_eval`` are on the E_L core path but their forward values (J and ln|Psi|) do not enter the E_L formula directly -(E_L depends on *derivatives* of ln|Psi|). Diagnostics show zero E_L -bias when these zones alone are fp32. +(E_L depends on *derivatives* of ln|Psi|), so fp32 in these zones alone +has no E_L impact. § ``ao_grad_lap`` is fp64 even in mixed mode because the analytic Laplacian kernel for spherical AOs contains catastrophic cancellation (``4 Z² r² − 6 Z`` and ``(safe_div − 2 Z·base)² − safe_div² − 2 Z`` -terms) that fp32 cannot resolve for tight Gaussians. Diagnostic -``bug/fp32/diag_07_ao_grad_vs_lap_split.py`` showed that -``ao_lap=fp32`` alone reproduces the full atomic-force bias -(``max|dF| ≈ 1.9 Ha/bohr`` on N₂ at scale=0.3, ``≈ 2e−2 Ha/bohr`` on -the water-cluster-8 system); the historical ``ao_grad=fp32`` zone was -safe in isolation (``max|dF| < 8e−3 Ha/bohr``) but is merged here with -``ao_lap`` because the fused ``compute_AOs_value_grad_lap`` kernel -shares one heavy expression (``exp / pow / phi / S_l_m``) across grad -and lap. Running that shared kernel at fp32 would break the lap path, -so the unified zone is fp64 always — a small extra cost on the -standalone ``compute_AOs_grad`` (which is not on the per-step hot -path) in exchange for a single source of truth for the shared kernel -dtype. +terms) that fp32 cannot resolve for tight Gaussians. ``ao_grad`` is +merged with ``ao_lap`` into this single zone because the fused +``compute_AOs_value_grad_lap`` kernel shares one heavy expression +(``exp / pow / phi / S_l_m``) across grad and lap. Running that shared +kernel at fp32 would break the lap path, so the unified zone is fp64 +always — a small extra cost on the standalone ``compute_AOs_grad`` +(which is not on the per-step hot path) in exchange for a single +source of truth for the shared kernel dtype. ‡ ``det_ratio`` and ``jastrow_ratio`` affect E_L **indirectly** through the ECP non-local potential, which evaluates Psi(R')/Psi(R) on a @@ -328,44 +323,35 @@ def _compute_AOs_kernel(aos_data, r_carts): # result up before any sensitive arithmetic. # jastrow_eval - smooth correlation function value (pre-exp). # jastrow_grad_lap - nabla J, nabla^2 J; smooth Jastrow factor, low -# cancellation. Diagnostics show bias < 8e-06 Ha -# at 32 electrons (0.05 kcal/mol margin ×11). -# Kept as a single zone (no grad/lap split) because -# both halves share the same fp32 risk profile and -# Jastrow grad/lap functions compute the two -# together (``compute_grads_and_laplacian_*``). +# cancellation. Kept as a single zone (no grad/lap +# split) because both halves share the same fp32 +# risk profile and Jastrow grad/lap functions +# compute the two together +# (``compute_grads_and_laplacian_*``). # jastrow_ratio - J(R')-J(R) log-ratio; smooth and well-behaved. -# Diagnostics show bias < 2e-06 Ha (margin ×44). # -# All other zones stay fp64 because numerical experiments (see -# bug/fp32 diagnostics) show fp32 in those zones produces -# unacceptable bias on E_L for ~32-electron systems, OR the -# kernel is cheap enough that fp32 is not worth the bias: +# All other zones stay fp64 because fp32 in those zones produces +# unacceptable bias on E_L, OR the kernel is cheap enough that fp32 +# is not worth the bias: # # ao_grad_lap - analytic gradient + Laplacian kernel for spherical/ # Cartesian AOs. Lap arithmetic contains catastrophic # cancellation (``4 Z² r² − 6 Z`` and # ``(safe_div − 2 Z·base)² − safe_div² − 2 Z``). -# diag_07 showed lap=fp32 alone yields max|dF| ≈ 1.9 -# Ha/bohr on N₂ (scale=0.3), reproducing the entire -# bias of grad+lap=fp32. fp64 mandatory. This zone -# merges the historical ``ao_grad`` (which was safe at -# fp32 in isolation) with ``ao_lap`` because the fused -# ``compute_AOs_value_grad_lap`` kernel evaluates -# ``exp / pow / phi / S_l_m`` once and reuses it across -# grad and lap; running the shared path at fp32 would -# break the lap output. +# fp64 mandatory. This zone merges grad and lap +# because the fused ``compute_AOs_value_grad_lap`` +# kernel evaluates ``exp / pow / phi / S_l_m`` once +# and reuses it across grad and lap; running the +# shared path at fp32 would break the lap output. # coulomb - sum of 1/r + ECP spherical quadrature. Cheap # (O(N_e^2) el-el + O(N_e * N_nuc) el-ion, vs -# O(N_e * N_ao) AO eval) but contributes the -# largest individual bias among fp32 candidates -# (~6e-5 Ha at 64e/512 AO). Cost/benefit favors fp64. +# O(N_e * N_ao) AO eval); cost/benefit favors fp64. # # mo_eval - mo_coef @ AO matmul feeds the determinant matrix; # fp32 here amplifies into log|det| errors of O(1). # det_eval - geminal matrix + log(det) + SVD; cancellation in -# log(det), SVD 1/s near-singular, ε≈1e-7 entries -# produce O(1) log|det| error. +# log(det), SVD 1/s near-singular entries produce +# O(1) log|det| error. # mo_grad / mo_lap / det_grad_lap # - second derivatives of ln|Psi|; cancellation-sensitive # on the determinant side (the AO-side fp32 is absorbed @@ -384,22 +370,22 @@ def _compute_AOs_kernel(aos_data, r_carts): # atomic_orbital.py "ao_eval": "float32", # low risk (heavy kernel) "ao_grad_lap": "float64", # high risk (catastrophic cancellation in 4Z²r²-6Z terms; - # unified zone — historical ao_grad was safe at fp32 but is merged with ao_lap so - # the fused compute_AOs_value_grad_lap kernel can share one heavy kernel at fp64) + # unified zone — grad and lap share the fused compute_AOs_value_grad_lap kernel, + # which must run at fp64 to protect the lap path) # molecular_orbital.py "mo_eval": "float64", # high risk (feeds det_eval) "mo_grad": "float64", # high risk "mo_lap": "float64", # high risk # jastrow_factor.py "jastrow_eval": "float32", # low risk - "jastrow_grad_lap": "float32", # low risk (smooth J; bias < 8e-06 Ha at 32e) - "jastrow_ratio": "float32", # low risk (smooth J ratio; bias < 2e-06 Ha at 32e) + "jastrow_grad_lap": "float32", # low risk (smooth J, no severe cancellation) + "jastrow_ratio": "float32", # low risk (smooth J ratio, no severe cancellation) # determinant.py "det_eval": "float64", # high risk (LU/det / SVD) "det_grad_lap": "float64", # high risk (kept unsplit for symmetry with jastrow) "det_ratio": "float64", # high risk (SM update error + ECP non-local ratio) # coulomb_potential.py - "coulomb": "float64", # cheap kernel + largest single fp32 bias (~6e-5 Ha) + "coulomb": "float64", # cheap kernel; cost/benefit favors fp64 # wavefunction.py "wf_eval": "float64", # high risk "wf_kinetic": "float64", # high risk diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index 85ad44f3..e194cf3f 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -2213,14 +2213,9 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A we can apply :func:`_reduce_primitives_to_aos` to the radial product (``N R``) FIRST, then multiply by the AO-level polynomial. This (1) shrinks the materialised pre-reduction - buffer from ``(num_ao_prim, n_elec)`` to ``(num_ao, n_elec)`` - — for cc-pVQZ on C6H6 that is 880→512 along axis 0 — and (2) - runs the static-unrolled :func:`_int_pow_unrolled_cart` loops - (the dominant ALU pipe consumer per HLO inspection) at AO - rank rather than primitive rank. NCU/HLO showed the previous - formulation materialising a 3.7 GB intermediate - (``f64[880, 8192, 64]``) feeding the bucket gathers — this - rewrite cuts that to 2.15 GB. + buffer from ``(num_ao_prim, n_elec)`` to ``(num_ao, n_elec)``, + and (2) runs the static-unrolled :func:`_int_pow_unrolled_cart` + loops at AO rank rather than primitive rank. """ dtype_jnp = get_dtype_jnp("ao_eval") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) @@ -2269,17 +2264,6 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A # (``n * x^(n-1)``, ``n(n-1) * x^(n-2)``) so the divisions and # ``where(base != 0)`` masks are no longer needed either. The eps is # therefore fully removed across the AO module. - # - # NOTE (perf, shell-wise unroll attempt 2026-05): grouping AOs by static - # ``(nx, ny, nz)`` triplets and emitting a direct multiply chain per - # group eliminates the L_MAX-deep ``where(e == k, ...)`` select tree, - # but the required permute-once / inverse-permute layout introduced an - # end-of-kernel ``inv_perm`` gather of shape ``(num_ao, n_walker, - # n_elec)`` (~2 GB at f64) that became the new dominant - # ``loop_gather_fusion_1`` kernel on GH200, slowing cart f64 from - # 24 ms → 41 ms (NSYS measured). The select-tree formulation below is - # therefore preferred until a layout that avoids the round-trip gather - # is found. P_l_nx_ny_nz_ao = ( _int_pow_unrolled_cart(x_ao, nx_ao, L_MAX) * _int_pow_unrolled_cart(y_ao, ny_ao, L_MAX) @@ -3204,9 +3188,9 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra is assembled at AO rank. Eliminates the ``(n_walker, num_ao_prim, n_elec, 3)`` prim-rank gradient - intermediate (5.5 GB on cc-pVQZ C6H6) that previously dominated - the kinetic_disc / kinetic_continuum HLO when the standalone - grad API is reached (e.g. via ``compute_AOs_grad`` callers). + intermediate that would otherwise dominate the kinetic_disc / + kinetic_continuum HLO when the standalone grad API is reached + (e.g. via ``compute_AOs_grad`` callers). """ dtype_jnp = get_dtype_jnp("ao_grad_lap") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) @@ -3403,17 +3387,12 @@ def _compute_AOs_value_grad_lap_cart( - 4\\, ZNR \\cdot (x \\partial_x P + y \\partial_y P + z \\partial_z P) + (4 r^2\\, Z^2NR - 6\\, ZNR) \\cdot P - Previously the kernel materialised ``phi``, ``Kx``, ``Ky``, ``Kz`` - as four ``(n_walker, num_ao_prim, n_elec)`` tuples (~7.4 GB on - cc-pVQZ C6H6) and reduced each to AO rank with a separate - ``_reduce_primitives_to_aos``; HLO dump showed a single - ``loop_multiply_reduce_select_fusion`` of type - ``(f64[8192,880,32], f64[8192,880,32], f64[8192,880,32], - f64[8192,880,32])`` — the dominant DRAM consumer of the kinetic - energy / local energy paths. This rewrite shrinks that to one - prim-rank reduce per radial moment (NR / ZNR / Z²NR) and runs - all polynomial work at AO rank (~144 channel for C6H6 vs 880 - prim). + The kernel performs one prim-rank reduce per radial moment + (NR / ZNR / Z²NR) and runs all polynomial work at AO rank. + This avoids the four ``(n_walker, num_ao_prim, n_elec)`` + prim-rank tuples (``phi``, ``Kx``, ``Ky``, ``Kz``) that would + otherwise dominate the kinetic energy / local energy DRAM + traffic. """ dtype_eval = get_dtype_jnp("ao_eval") dtype_jnp = get_dtype_jnp("ao_grad_lap") @@ -3612,7 +3591,10 @@ def compute_AOs_value_grad_lap( ``S_l_m``, ``phi`` / ``pref``) is computed once in ``ao_grad_lap`` (fp64); ``val`` is downcast to ``ao_eval`` only at the segment-sum site. ``gx`` / ``gy`` / ``gz`` / ``lap`` are kept in fp64 to protect - the laplacian's ``4 Z^2 r^2 - 6 Z`` cancellation. + the laplacian's ``4 Z^2 r^2 - 6 Z`` cancellation. The val downcast + matches the standalone ``compute_AOs`` zone semantics so that + forward (det_eval) and grad/lap (det_grad_lap) consumers see val of + the same fp32 quality and remain variationally consistent. Args: aos_data: ``AOs_cart_data`` or ``AOs_sphe_data`` describing primitive diff --git a/jqmc/determinant.py b/jqmc/determinant.py index e25911f0..7f25bca5 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -1895,20 +1895,19 @@ def compute_grads_and_laplacian_ln_Det( lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) - # Explicitly upcast AO/MO forward values to the kinetic zone - # (compute_orb_api may return ao_eval / mo_eval dtype, e.g. fp32 for AGP) to avoid - # relying on JAX implicit type promotion in the lambda/gradient matmuls below. - ao_matrix_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) - ao_matrix_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) - - ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z = geminal_data.compute_orb_grad_api( - geminal_data.orb_data_up_spin, r_up_carts + # Single fused dispatch shares the heavy block (exp / poly / S_l_m) across + # val/grad/lap and matches the ``_fast`` / ``_grads_lap_body`` paths so all + # three produce bit-identical AO outputs. Cast val to det_grad_lap zone + # (fp64) at the use site below — required by Principle 3b since the kernel + # zone for downstream einsums is det_grad_lap. + ao_matrix_up, ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z, ao_matrix_laplacian_up = ( + geminal_data.compute_orb_value_grad_lap_api(geminal_data.orb_data_up_spin, r_up_carts) ) - ao_matrix_dn_grad_x, ao_matrix_dn_grad_y, ao_matrix_dn_grad_z = geminal_data.compute_orb_grad_api( - geminal_data.orb_data_dn_spin, r_dn_carts + ao_matrix_dn, ao_matrix_dn_grad_x, ao_matrix_dn_grad_y, ao_matrix_dn_grad_z, ao_matrix_laplacian_dn = ( + geminal_data.compute_orb_value_grad_lap_api(geminal_data.orb_data_dn_spin, r_dn_carts) ) - ao_matrix_laplacian_up = geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_up_spin, r_up_carts) - ao_matrix_laplacian_dn = geminal_data.compute_orb_laplacian_api(geminal_data.orb_data_dn_spin, r_dn_carts) + ao_matrix_up = ao_matrix_up.astype(dtype_jnp) + ao_matrix_dn = ao_matrix_dn.astype(dtype_jnp) ao_up_grads = jnp.stack([ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z], axis=0) ao_dn_grads = jnp.stack([ao_matrix_dn_grad_x, ao_matrix_dn_grad_y, ao_matrix_dn_grad_z], axis=0) diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index ae7e47b8..f6749283 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -2640,9 +2640,9 @@ def _batch_pairwise_sum(points_a, points_b, param): # Use tensordot with explicit contracting axes (n_ao = axis 0 of both # operands) instead of ``aos_p_batch.T @ W_up``: under vmap on the # walker axis XLA's transpose-folding does not fold the ``.T`` into - # the dot, materialising an explicit ~1.8 GB ``transpose`` kernel - # (HBM-bound, ~88-92% DRAM peak on GH200). Expressing the contraction - # via ``dot_general(contract=[0]x[0])`` avoids the materialisation. + # the dot, materialising an explicit ``transpose`` kernel that is + # HBM-bound. Expressing the contraction via + # ``dot_general(contract=[0]x[0])`` avoids the materialisation. V_up = jnp.tensordot(aos_p_batch, W_up, axes=((0,), (0,))) # (N, N_up) P_up = jnp.dot(U_up, aos_p_batch) # (N_up, N) Q_up_c = (idx_for_Q[:, None] < jnp.arange(num_up)[None, :]).astype(dtype_jnp) # (N, N_up) diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 430993c8..9b2ffe75 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -1534,7 +1534,7 @@ def _compute_local_energy_t( # change between branching steps within a single ``run()``. # If these were defined inside the per-branching loop, every step # would create a fresh function identity and trigger a full - # re-trace + re-compile (causing ~7 s/step on H100). + # re-trace + re-compile. # ------------------------------------------------------------------ def _cond_t(carry): tau_left = carry[2] @@ -1721,12 +1721,9 @@ def _run_projection_loop_streaming(pcl, tll, wll, ru, rd, Ainv, key, ks): # dispatched ``vmap(_projection_t)`` from the host once per # projection step and broke on ``np.max(tau_left_list) <= 0``. # That ``np.max`` forces a host-side jax->numpy materialization, - # which blocks on the GPU once per step (so 27 projections => - # 27 host syncs and 27 jit dispatches per branching). On H100 - # this dominates wall time for small systems (e.g. 01water: - # ~4.5 ms/step vs <0.5 ms/step measured for GFMC_n which uses - # ``lax.fori_loop``). We replace it with ``lax.while_loop`` so - # the entire projection loop is captured into a single jit graph + # which blocks on the GPU once per step and dispatches a fresh + # jit per step. We replace it with ``lax.while_loop`` so the + # entire projection loop is captured into a single jit graph # (CUDA-graph friendly) and only one host sync happens at the # end via ``block_until_ready`` below. The cond is evaluated on # device (``jnp.max(tau_left) > 0.0``). diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index 23efbda4..3c378909 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -252,7 +252,7 @@ def compute_MOs(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: # but the (small) MO matmul and the returned MO matrix are kept in the # ``determinant`` precision (fp64 by default). This avoids amplifying fp32 # round-off through downstream determinant / kinetic / energy paths while - # preserving the speed of the AO kernels (see bug/fp32 diagnostics). + # preserving the speed of the AO kernels. out_dtype = get_dtype_jnp("mo_eval") aos = compute_AOs(aos_data=mos_data.aos_data, r_carts=r_carts).astype(out_dtype) mo_coefficients = mos_data._mo_coefficients_jnp.astype(out_dtype) From f66cfa31867aaf69a64914a0c40b3678c0641012 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 00:51:35 +0900 Subject: [PATCH 43/97] Clean up code and docstrings: Remove non-ASCII characters and enhance ruff tools. Fix many violating parts. --- .github/workflows/jqmc-run-full-pytest.yml | 17 ++- .github/workflows/jqmc-run-long-pytest.yml | 17 ++- .github/workflows/jqmc-run-rc-pytest.yml | 12 ++ .github/workflows/jqmc-run-short-pytest.yml | 17 ++- .pre-commit-config.yaml | 20 ++- README.md | 2 + benchmarks/profile_bottleneck.py | 2 +- doc/conf.py | 6 +- .../jqmc-example06/how_to_use_modules.ipynb | 2 +- .../run_pes_pipeline.py | 36 ++--- .../jqmc-workflow-example02/run_pipelines.py | 32 ++--- .../jqmc-workflow-example03/run_pipelines.py | 26 ++-- .../run_test_on_local.py | 58 ++++---- jqmc/_checkpoint.py | 20 +-- jqmc/_jqmc_utility.py | 38 +++--- jqmc/_precision.py | 70 +++++----- jqmc/_setting.py | 8 +- jqmc/atomic_orbital.py | 96 ++++++------- jqmc/coulomb_potential.py | 78 +++++------ jqmc/determinant.py | 88 ++++++------ jqmc/hamiltonians.py | 16 +-- jqmc/jastrow_factor.py | 100 +++++++------- jqmc/jqmc_gfmc.py | 64 ++++----- jqmc/jqmc_mcmc.py | 118 ++++++++-------- jqmc/molecular_orbital.py | 16 +-- jqmc/obsolete/qmc_kernel.py | 28 ++-- jqmc/obsolete/vmc_vectorized.py | 2 +- jqmc/structure.py | 2 +- jqmc/swct.py | 4 +- jqmc/wavefunction.py | 30 ++-- jqmc_workflow/__init__.py | 6 +- jqmc_workflow/_error_estimator.py | 12 +- jqmc_workflow/_job.py | 30 ++-- jqmc_workflow/_lrdmc_calibration.py | 12 +- jqmc_workflow/_machine.py | 28 ++-- jqmc_workflow/_output_parser.py | 128 +++++++++--------- jqmc_workflow/_phase.py | 4 +- jqmc_workflow/_results.py | 52 +++---- jqmc_workflow/_state.py | 68 +++++----- jqmc_workflow/_transfer.py | 14 +- jqmc_workflow/launcher.py | 24 ++-- jqmc_workflow/lrdmc_ext_workflow.py | 24 ++-- jqmc_workflow/lrdmc_workflow.py | 82 +++++------ jqmc_workflow/mcmc_workflow.py | 56 ++++---- jqmc_workflow/vmc_workflow.py | 54 ++++---- jqmc_workflow/wf_workflow.py | 12 +- jqmc_workflow/workflow.py | 94 ++++++------- prototypes/block_diagonal_matrix.py | 6 +- prototypes/vmc_vmap.py | 8 +- pyproject.toml | 70 +++++++++- scripts/check_ascii.sh | 33 +++++ tests/test_AOs.py | 4 +- tests/test_MOs.py | 6 +- tests/test_ao_basis_optimization.py | 2 +- tests/test_checkpoint_components.py | 8 +- tests/test_checkpoint_gfmc.py | 6 +- tests/test_checkpoint_mcmc.py | 10 +- tests/test_determinant.py | 24 ++-- tests/test_hamiltonian.py | 2 +- tests/test_init_electron_configurations.py | 4 +- tests/test_jastrow.py | 28 ++-- tests/test_jqmc_mcmc.py | 34 ++--- tests/test_jqmc_tool.py | 2 +- tests/test_mixed_precision.py | 18 +-- tests/test_wave_function.py | 6 +- 65 files changed, 1065 insertions(+), 931 deletions(-) create mode 100755 scripts/check_ascii.sh diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index 0739fa65..abf4f66a 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -47,12 +47,17 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - #- name: Lint jqmc with flake8 - # run: | - # # stop the build if there are Python syntax errors or undefined names - # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Lint (pre-commit, currently-enforced hooks) + # Runs hooks declared in .pre-commit-config.yaml: + # - ruff (RUF001/002/003 ambiguous-unicode only, --no-fix) + # - ruff-format + # - ascii-only (scripts/check_ascii.sh) + # - trailing-whitespace, end-of-file-fixer, check-added-large-files + # To enforce additional ruff rules, fix the violations listed in + # `lint.extend-ignore` in pyproject.toml and remove them from that list. + run: | + python -m pip install pre-commit + pre-commit run --all-files - name: Test jqmc FP64 (intra-software comparisons) run: | diff --git a/.github/workflows/jqmc-run-long-pytest.yml b/.github/workflows/jqmc-run-long-pytest.yml index e526f38a..6696289f 100644 --- a/.github/workflows/jqmc-run-long-pytest.yml +++ b/.github/workflows/jqmc-run-long-pytest.yml @@ -47,12 +47,17 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - #- name: Lint jqmc with flake8 - # run: | - # # stop the build if there are Python syntax errors or undefined names - # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Lint (pre-commit, currently-enforced hooks) + # Runs hooks declared in .pre-commit-config.yaml: + # - ruff (RUF001/002/003 ambiguous-unicode only, --no-fix) + # - ruff-format + # - ascii-only (scripts/check_ascii.sh) + # - trailing-whitespace, end-of-file-fixer, check-added-large-files + # To enforce additional ruff rules, fix the violations listed in + # `lint.extend-ignore` in pyproject.toml and remove them from that list. + run: | + python -m pip install pre-commit + pre-commit run --all-files - name: Test jqmc FP64/FP32+FP64 (intra-software comparisons) run: | diff --git a/.github/workflows/jqmc-run-rc-pytest.yml b/.github/workflows/jqmc-run-rc-pytest.yml index 95f8b433..1a5009fa 100644 --- a/.github/workflows/jqmc-run-rc-pytest.yml +++ b/.github/workflows/jqmc-run-rc-pytest.yml @@ -42,6 +42,18 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Lint (pre-commit, currently-enforced hooks) + # Runs hooks declared in .pre-commit-config.yaml: + # - ruff (RUF001/002/003 ambiguous-unicode only, --no-fix) + # - ruff-format + # - ascii-only (scripts/check_ascii.sh) + # - trailing-whitespace, end-of-file-fixer, check-added-large-files + # To enforce additional ruff rules, fix the violations listed in + # `lint.extend-ignore` in pyproject.toml and remove them from that list. + run: | + python -m pip install pre-commit + pre-commit run --all-files + - name: Install jqmc run: | python -m pip install flake8 pytest pytest-cov diff --git a/.github/workflows/jqmc-run-short-pytest.yml b/.github/workflows/jqmc-run-short-pytest.yml index 004bb96b..d4d00807 100644 --- a/.github/workflows/jqmc-run-short-pytest.yml +++ b/.github/workflows/jqmc-run-short-pytest.yml @@ -58,12 +58,17 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - #- name: Lint jqmc with flake8 - # run: | - # # stop the build if there are Python syntax errors or undefined names - # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Lint (pre-commit, currently-enforced hooks) + # Runs hooks declared in .pre-commit-config.yaml: + # - ruff (RUF001/002/003 ambiguous-unicode only, --no-fix) + # - ruff-format + # - ascii-only (scripts/check_ascii.sh) + # - trailing-whitespace, end-of-file-fixer, check-added-large-files + # To enforce additional ruff rules, fix the violations listed in + # `lint.extend-ignore` in pyproject.toml and remove them from that list. + run: | + python -m pip install pre-commit + pre-commit run --all-files - name: Test jqmc FP64 (intra-software comparisons) run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9d4ecbeb..ad250783 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,22 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.12 hooks: - #- id: ruff - # args: [ "--fix", "--show-fixes" ] + - id: ruff + name: ruff (ambiguous unicode only) + args: ["--select", "RUF001,RUF002,RUF003", "--no-fix"] - id: ruff-format + +- repo: local + hooks: + - id: ascii-only + name: Reject non-ASCII bytes in Python sources + entry: scripts/check_ascii.sh + language: system + types: [python] + exclude: '^(jqmc/obsolete/|prototypes/)' + pass_filenames: true + - id: ascii-only-commit-msg + name: Reject non-ASCII bytes in commit message + entry: scripts/check_ascii.sh + language: system + stages: [commit-msg] diff --git a/README.md b/README.md index 5262e1d8..bb4cd8d8 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ git-commit. Pre-commit is set-up and used in the following way: - Installed by `pip install pre-commit`, `conda install pre_commit` or see https://pre-commit.com/#install. - pre-commit hook is installed by `pre-commit install`. +- commit-msg hook (rejects non-ASCII bytes in commit messages) is + installed by `pre-commit install --hook-type commit-msg`. - pre-commit hook is run by `pre-commit run --all-files`. Unless running pre-commit, pre-commit.ci may push the fix at PR by github diff --git a/benchmarks/profile_bottleneck.py b/benchmarks/profile_bottleneck.py index 5e1cc1de..7f82f8b9 100644 --- a/benchmarks/profile_bottleneck.py +++ b/benchmarks/profile_bottleneck.py @@ -116,7 +116,7 @@ def timefull(fn, *a): print(f" jastrow ratio: {t_jas:.3f} ms") print(f" combined: {t_det + t_jas:.3f} ms (kinetic full = {t_kin:.3f} ms)") -# ── 3b Jastrow breakdown ────────────────────────────────────────────────────── +# -- 3b Jastrow breakdown ------------------------------------------------------ trexio_file2 = TREXIO_FILE _, aos_data_for_j3, _, _, _, coulomb_data2 = read_trexio_file(trexio_file2, store_tuple=True) diff --git a/doc/conf.py b/doc/conf.py index 84bea046..2fa9827d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,7 +37,7 @@ def _generate_examples_page(): """Scan ``examples/jqmc-*/README.md`` and emit ``doc/examples.md``. - * Heading levels are bumped by one (``#`` → ``##``). + * Heading levels are bumped by one (``#`` -> ``##``). * Image paths are rewritten to be relative to ``doc/``. * Runs at import time so the file is ready before Sphinx reads sources. """ @@ -62,7 +62,7 @@ def _generate_examples_page(): content = readme.read_text("utf-8") - # Bump heading levels: # → ##, ## → ###, etc. + # Bump heading levels: # -> ##, ## -> ###, etc. content = _re.sub(r"^(#+)", r"#\1", content, flags=_re.MULTILINE) # Fix image paths (skip absolute URLs) @@ -84,7 +84,7 @@ def _dedup_footnote(m): content = _re.sub(r"^\[\^([^\]]+)\]:.*$", _dedup_footnote, content, flags=_re.MULTILINE) - # Strip ":filename" from fenced code block language (e.g. ```toml:vmc.toml → ```toml) + # Strip ":filename" from fenced code block language (e.g. ```toml:vmc.toml -> ```toml) content = _re.sub(r"^(```\w+):[^\s`]+", r"\1", content, flags=_re.MULTILINE) sections.append(content.rstrip()) diff --git a/examples/jqmc-example06/how_to_use_modules.ipynb b/examples/jqmc-example06/how_to_use_modules.ipynb index 960cab88..6968be4d 100644 --- a/examples/jqmc-example06/how_to_use_modules.ipynb +++ b/examples/jqmc-example06/how_to_use_modules.ipynb @@ -1087,7 +1087,7 @@ } ], "source": [ - "geminal_data.get_info()" + "geminal_mo_data.get_info()" ] }, { diff --git a/examples/jqmc-workflow-example01/run_pes_pipeline.py b/examples/jqmc-workflow-example01/run_pes_pipeline.py index 82d948ad..c5230fe2 100644 --- a/examples/jqmc-workflow-example01/run_pes_pipeline.py +++ b/examples/jqmc-workflow-example01/run_pes_pipeline.py @@ -1,4 +1,4 @@ -"""H2 PES pipeline: pySCF → WF → VMC (JSD, MO opt) → MCMC + LRDMC (a=0.2). +"""H2 PES pipeline: pySCF -> WF -> VMC (JSD, MO opt) -> MCMC + LRDMC (a=0.2). For each bond length R, this script: 1. Runs pySCF locally to produce a TREXIO HDF5 file. @@ -7,7 +7,7 @@ 4. Launches MCMC and LRDMC (a=0.2) production runs (in parallel). 5. Prints a summary table with energies and atomic forces. -All R values are independent; their WF → VMC → {MCMC, LRDMC} chains +All R values are independent; their WF -> VMC -> {MCMC, LRDMC} chains run in parallel once the DAG is submitted to the Launcher. """ @@ -37,7 +37,7 @@ parse_mcmc_output, ) -# ── Configuration ───────────────────────────────────────────────── +# -- Configuration ------------------------------------------------- SERVER = "cluster" QUEUE_LABEL = "cores-120-mpi-120-omp-1-1h" @@ -77,7 +77,7 @@ 1.40, ] -# ── pySCF script template ──────────────────────────────────────── +# -- pySCF script template ---------------------------------------- PYSCF_TEMPLATE = '''\ from pyscf import gto, scf from pyscf.tools import trexio @@ -109,7 +109,7 @@ ''' -# ── Helpers ─────────────────────────────────────────────────────── +# -- Helpers ------------------------------------------------------- def r_dir(R: float) -> str: """Return the directory name for a given bond length.""" return f"R_{R:.2f}" @@ -158,7 +158,7 @@ def format_force(force: float | None, error: float | None) -> str: def extract_max_force_norm( forces: list[dict] | None, ) -> tuple[float | None, float | None]: - """Return (|F|_max, err) — max force norm over atoms. + """Return (|F|_max, err) -- max force norm over atoms. *forces* is ``[{"Fx": .., "Fx_err": .., "Fy": .., ...}, ...]``. """ @@ -182,7 +182,7 @@ def extract_max_force_norm( return best_norm, best_err -# ── Step 1: Run pySCF locally ──────────────────────────────────── +# -- Step 1: Run pySCF locally ------------------------------------ def run_pyscf_calculations(base_dir: str) -> dict[float, float | None]: """Run pySCF for each R value and return HF energies.""" print("=" * 60) @@ -229,7 +229,7 @@ def run_pyscf_calculations(base_dir: str) -> dict[float, float | None]: return hf_energies -# ── Step 2: Build WF → VMC → {MCMC, LRDMC} pipeline ───────────── +# -- Step 2: Build WF -> VMC -> {MCMC, LRDMC} pipeline ------------- def build_pipeline() -> tuple[list[Container], dict[float, Container], dict[float, Container]]: """Build Container list for all R values. @@ -246,7 +246,7 @@ def build_pipeline() -> tuple[list[Container], dict[float, Container], dict[floa label_lrdmc = f"lrdmc-{R:.2f}" trexio_file = trexio_filename(R) - # WF: TREXIO → hamiltonian_data.h5 (JSD: J1 + J2 + J3-MO) + # WF: TREXIO -> hamiltonian_data.h5 (JSD: J1 + J2 + J3-MO) wf = Container( label=label_wf, dirname=os.path.join(r_dir(R), "01_wf"), @@ -347,7 +347,7 @@ def build_pipeline() -> tuple[list[Container], dict[float, Container], dict[floa return workflows, mcmc_containers, lrdmc_containers -# ── Step 3: Print summary table ────────────────────────────────── +# -- Step 3: Print summary table ---------------------------------- def print_summary_table( hf_energies: dict[float, float | None], mcmc_containers: dict[float, Container], @@ -362,13 +362,13 @@ def print_summary_table( print() header = ( - f"| {'R (Å)':>6} " + f"| {'R (A)':>6} " f"| {'E_HF (Ha)':>13} " f"| {'E_MCMC (Ha)':>15} " - f"| {'F_MCMC (Ha/Å)':>15} " + f"| {'F_MCMC (Ha/A)':>15} " f"| {'MCMC t_net':>10} " f"| {'E_LRDMC (Ha)':>15} " - f"| {'F_LRDMC (Ha/Å)':>16} " + f"| {'F_LRDMC (Ha/A)':>16} " f"| {'LRDMC t_net':>11} |" ) separator = f"|{'-' * 8}|{'-' * 15}|{'-' * 17}|{'-' * 17}|{'-' * 12}|{'-' * 17}|{'-' * 18}|{'-' * 13}|" @@ -420,8 +420,8 @@ def print_summary_table( print() -# ── Step 4: Plot PES ────────────────────────────────────────────── -BOHR_PER_ANG = 1.8897259886 # 1 Å = 1.8897 bohr +# -- Step 4: Plot PES ---------------------------------------------- +BOHR_PER_ANG = 1.8897259886 # 1 A = 1.8897 bohr plt.rcParams["font.family"] = "sans-serif" plt.rcParams["xtick.direction"] = "in" @@ -553,7 +553,7 @@ def plot_pes(base_dir: str) -> str: return out_pdf -# ── Main ────────────────────────────────────────────────────────── +# -- Main ---------------------------------------------------------- if __name__ == "__main__": base_dir = os.path.dirname(os.path.abspath(__file__)) os.chdir(base_dir) @@ -561,10 +561,10 @@ def plot_pes(base_dir: str) -> str: # 1) pySCF (local) hf_energies = run_pyscf_calculations(base_dir) - # 2) WF → VMC → {MCMC, LRDMC} (via jqmc-workflow) + # 2) WF -> VMC -> {MCMC, LRDMC} (via jqmc-workflow) print() print("=" * 60) - print(" Step 2: WF → VMC → MCMC + LRDMC (via jqmc-workflow)") + print(" Step 2: WF -> VMC -> MCMC + LRDMC (via jqmc-workflow)") print("=" * 60) workflows, mcmc_containers, lrdmc_containers = build_pipeline() diff --git a/examples/jqmc-workflow-example02/run_pipelines.py b/examples/jqmc-workflow-example02/run_pipelines.py index 244445a5..53d2d98e 100644 --- a/examples/jqmc-workflow-example02/run_pipelines.py +++ b/examples/jqmc-workflow-example02/run_pipelines.py @@ -1,4 +1,4 @@ -"""Walker-scaling benchmark: pySCF → WF → VMC → MCMC + LRDMC. +"""Walker-scaling benchmark: pySCF -> WF -> VMC -> MCMC + LRDMC. For a water molecule (ccECP, cc-pVTZ), this script benchmarks vectorization efficiency by sweeping the ``number_of_walkers`` @@ -6,7 +6,7 @@ Pipeline -------- - 1. Run pySCF locally → TREXIO file. + 1. Run pySCF locally -> TREXIO file. 2. Convert to ``hamiltonian_data.h5`` with JSD Jastrow (WF_Workflow). 3. Optimize J1/J2/J3 via VMC_Workflow. 4. For **each walker count**, launch: @@ -17,7 +17,7 @@ Step-count control ------------------ MCMC and LRDMC use an **explicit** number of measurement steps -rather than target-error–based automatic convergence. Internally +rather than target-error-based automatic convergence. Internally we set ``pilot_steps = NUM_MCMC_STEPS`` and a large ``target_error`` (999 Ha) so that (a) the pilot phase runs with NUM_MCMC_STEPS and (b) the production run uses the same step count (since @@ -50,7 +50,7 @@ parse_mcmc_output, ) -# ── Configuration ───────────────────────────────────────────────── +# -- Configuration ------------------------------------------------- SERVER = "cluster" QUEUE_LABEL_s = "cores-4-mpi-4-gpu-4-omp-1-30m" QUEUE_LABEL_l = "cores-4-mpi-4-gpu-4-omp-1-3h" @@ -107,7 +107,7 @@ ''' -# ── Helpers ─────────────────────────────────────────────────────── +# -- Helpers ------------------------------------------------------- def extract_hf_energy(pyscf_output: str) -> float | None: """Parse the converged SCF energy from pySCF output.""" if not os.path.isfile(pyscf_output): @@ -132,7 +132,7 @@ def format_energy(energy: float | None, error: float | None) -> str: return f"{energy:.{n_dec}f}({err_in_last})" -# ── Step 1: Run pySCF locally ──────────────────────────────────── +# -- Step 1: Run pySCF locally ------------------------------------ def run_pyscf(base_dir: str) -> float | None: """Run pySCF for water and return HF energy.""" print("=" * 60) @@ -173,7 +173,7 @@ def run_pyscf(base_dir: str) -> float | None: return hf_energy -# ── Step 2: Build WF → VMC → {MCMC, LRDMC} × walkers pipeline ── +# -- Step 2: Build WF -> VMC -> {MCMC, LRDMC} x walkers pipeline -- def build_pipeline() -> tuple[ list[Container], dict[int, Container], @@ -188,8 +188,8 @@ def build_pipeline() -> tuple[ mcmc_containers: dict[int, Container] = {} lrdmc_containers: dict[int, Container] = {} - # ── Common stages (run once) ────────────────────────────────── - # WF: TREXIO → hamiltonian_data.h5 (JSD: J1 + J2 + J3-ao-small) + # -- Common stages (run once) ---------------------------------- + # WF: TREXIO -> hamiltonian_data.h5 (JSD: J1 + J2 + J3-ao-small) wf = Container( label="wf", dirname="01_wf", @@ -239,7 +239,7 @@ def build_pipeline() -> tuple[ workflows.extend([wf, vmc]) - # ── Per-walker-count stages ─────────────────────────────────── + # -- Per-walker-count stages ----------------------------------- for nw in WALKER_COUNTS: label_mcmc = f"mcmc-w{nw}" label_lrdmc = f"lrdmc-w{nw}" @@ -261,7 +261,7 @@ def build_pipeline() -> tuple[ num_mcmc_warmup_steps=25, num_mcmc_bin_blocks=10, # Explicit step count: pilot_steps = desired steps, - # target_error = huge → production uses same count. + # target_error = huge -> production uses same count. num_mcmc_steps=NUM_MCMC_STEPS_MCMC, max_time=3000, poll_interval=300, @@ -303,7 +303,7 @@ def build_pipeline() -> tuple[ return workflows, mcmc_containers, lrdmc_containers -# ── Step 3: Print summary table ────────────────────────────────── +# -- Step 3: Print summary table ---------------------------------- def print_summary_table( base_dir: str, hf_energy: float | None, @@ -364,7 +364,7 @@ def print_summary_table( print() -# ── Step 4: Plot throughput ─────────────────────────────────────── +# -- Step 4: Plot throughput --------------------------------------- def plot_throughput(base_dir: str) -> None: """Plot normalized throughput vs number of walkers. @@ -445,7 +445,7 @@ def plot_throughput(base_dir: str) -> None: print(f" Throughput plot saved to {out_path}") -# ── Main ────────────────────────────────────────────────────────── +# -- Main ---------------------------------------------------------- if __name__ == "__main__": base_dir = os.path.dirname(os.path.abspath(__file__)) os.chdir(base_dir) @@ -453,10 +453,10 @@ def plot_throughput(base_dir: str) -> None: # 1) pySCF (local) hf_energy = run_pyscf(base_dir) - # 2) WF → VMC → {MCMC, LRDMC} × walkers (via jqmc-workflow) + # 2) WF -> VMC -> {MCMC, LRDMC} x walkers (via jqmc-workflow) print() print("=" * 60) - print(" Step 2: WF → VMC → MCMC + LRDMC (via jqmc-workflow)") + print(" Step 2: WF -> VMC -> MCMC + LRDMC (via jqmc-workflow)") print("=" * 60) workflows, mcmc_containers, lrdmc_containers = build_pipeline() diff --git a/examples/jqmc-workflow-example03/run_pipelines.py b/examples/jqmc-workflow-example03/run_pipelines.py index 8bb9b0fb..85deec14 100644 --- a/examples/jqmc-workflow-example03/run_pipelines.py +++ b/examples/jqmc-workflow-example03/run_pipelines.py @@ -9,8 +9,8 @@ Patterns -------- - A) J3 + MCMC — SR use_lm=True, delta=0.35 - B) J3 + LRDMC — same VMC; LRDMC a=0.30 + A) J3 + MCMC -- SR use_lm=True, delta=0.35 + B) J3 + LRDMC -- same VMC; LRDMC a=0.30 """ import math @@ -31,7 +31,7 @@ parse_mcmc_output, ) -# ── Configuration ───────────────────────────────────────────────── +# -- Configuration ------------------------------------------------- SERVER = "cluster" QUEUE_LABEL = "cores-120-mpi-120-omp-1-24h" PILOT_QUEUE_LABEL = "cores-120-mpi-120-omp-1-3h" @@ -88,7 +88,7 @@ ''' -# ── Helpers ─────────────────────────────────────────────────────── +# -- Helpers ------------------------------------------------------- def extract_hf_energy(pyscf_output: str) -> float | None: """Parse the converged SCF energy from pySCF output.""" if not os.path.isfile(pyscf_output): @@ -113,7 +113,7 @@ def format_val(val: float | None, err: float | None) -> str: return f"{val:.{n_dec}f}({err_in_last})" -# ── Step 0: Run pySCF locally ──────────────────────────────────── +# -- Step 0: Run pySCF locally ------------------------------------ def run_pyscf(base_dir: str) -> float | None: """Run pySCF for water and return HF energy.""" print("=" * 60) @@ -153,7 +153,7 @@ def run_pyscf(base_dir: str) -> float | None: return hf_energy -# ── Step 1: Build pipelines for J3 + MCMC / LRDMC ──────────────── +# -- Step 1: Build pipelines for J3 + MCMC / LRDMC ---------------- def build_pipeline() -> tuple[ list[Container], dict[str, Container], @@ -171,10 +171,10 @@ def build_pipeline() -> tuple[ result_containers: dict[str, Container] = {} # ================================================================== - # J2=exp + J3=ao-small → MCMC + LRDMC + # J2=exp + J3=ao-small -> MCMC + LRDMC # ================================================================== - # WF: TREXIO → hamiltonian_data.h5 (J1=None, J2=exp, J3=ao-small) + # WF: TREXIO -> hamiltonian_data.h5 (J1=None, J2=exp, J3=ao-small) wf = Container( label="wf", dirname="01_wf", @@ -281,7 +281,7 @@ def build_pipeline() -> tuple[ return workflows, result_containers -# ── Step 2: Print summary table ────────────────────────────────── +# -- Step 2: Print summary table ---------------------------------- def print_summary_table( hf_energy: float | None, result_containers: dict[str, Container], @@ -321,7 +321,7 @@ def print_summary_table( e_str = format_val(e, e_err) - # Max |F| = max over atoms of sqrt(Fx² + Fy² + Fz²) + # Max |F| = max over atoms of sqrt(Fx^2 + Fy^2 + Fz^2) f_val, f_err = None, None if forces: max_norm = -1.0 @@ -349,7 +349,7 @@ def print_summary_table( print() -# ── Main ────────────────────────────────────────────────────────── +# -- Main ---------------------------------------------------------- if __name__ == "__main__": base_dir = os.path.dirname(os.path.abspath(__file__)) os.chdir(base_dir) @@ -357,10 +357,10 @@ def print_summary_table( # 0) pySCF (local) hf_energy = run_pyscf(base_dir) - # 1) WF → VMC → {MCMC, LRDMC} + # 1) WF -> VMC -> {MCMC, LRDMC} print() print("=" * 60) - print(" Step 1: WF → VMC → MCMC + LRDMC (J3)") + print(" Step 1: WF -> VMC -> MCMC + LRDMC (J3)") print("=" * 60) workflows, result_containers = build_pipeline() diff --git a/examples/jqmc-workflow-example99/run_test_on_local.py b/examples/jqmc-workflow-example99/run_test_on_local.py index c415cbac..00894eac 100644 --- a/examples/jqmc-workflow-example99/run_test_on_local.py +++ b/examples/jqmc-workflow-example99/run_test_on_local.py @@ -1,21 +1,21 @@ #!/usr/bin/env python """Local integration test for jqmc_workflow. -H₂ at 2 bond lengths (R = 0.74, 1.00 Å) — all-electron, cc-pVTZ, JSD. It should run **locally**. +H_2 at 2 bond lengths (R = 0.74, 1.00 A) -- all-electron, cc-pVTZ, JSD. It should run **locally**. Pipeline per R: - pySCF (DFT) → WF (JSD: J1-exp + J2 + J3-ao-small) → VMC (15 opt steps) → MCMC - → LRDMC_t (GFMC_t, a=0.30) - → LRDMC_n (GFMC_n, a=0.30, survival_ratio=0.95) + pySCF (DFT) -> WF (JSD: J1-exp + J2 + J3-ao-small) -> VMC (15 opt steps) -> MCMC + -> LRDMC_t (GFMC_t, a=0.30) + -> LRDMC_n (GFMC_n, a=0.30, survival_ratio=0.95) After the pipeline completes, the script exercises the new Phase-1 APIs: -* ``get_all_workflow_statuses()`` — list every workflow_state.toml -* ``get_workflow_summary()`` — detailed summary per directory -* ``parse_vmc_output()`` — per-step VMC diagnostic data -* ``parse_mcmc_output()`` — MCMC diagnostic data -* ``parse_lrdmc_output()`` — LRDMC diagnostic data -* ``parse_input_params()`` — TOML parameter extraction +* ``get_all_workflow_statuses()`` -- list every workflow_state.toml +* ``get_workflow_summary()`` -- detailed summary per directory +* ``parse_vmc_output()`` -- per-step VMC diagnostic data +* ``parse_mcmc_output()`` -- MCMC diagnostic data +* ``parse_lrdmc_output()`` -- LRDMC diagnostic data +* ``parse_input_params()`` -- TOML parameter extraction """ import dataclasses @@ -40,7 +40,7 @@ parse_vmc_output, ) -# ── Configuration ───────────────────────────────────────────────── +# -- Configuration ------------------------------------------------- SERVER = "cluster" QUEUE_LABEL = "qM" @@ -88,7 +88,7 @@ ''' -# ── Helpers ─────────────────────────────────────────────────────── +# -- Helpers ------------------------------------------------------- def r_dir(R: float) -> str: return f"R_{R:.2f}" @@ -97,7 +97,7 @@ def trexio_filename(R: float) -> str: return f"H2_R_{R:.2f}.h5" -# ── Step 0: pySCF (local) ──────────────────────────────────────── +# -- Step 0: pySCF (local) ---------------------------------------- def run_pyscf(base_dir: str) -> None: """Run pySCF for each R value.""" print("=" * 60) @@ -130,9 +130,9 @@ def run_pyscf(base_dir: str) -> None: print(f" [done] {r_dir(R)}/00_pyscf/{trexio_filename(R)}") -# ── Step 1: Build pipeline ─────────────────────────────────────── +# -- Step 1: Build pipeline --------------------------------------- def build_pipeline() -> list[Container]: - """Build WF → VMC → {MCMC, LRDMC} for each R value.""" + """Build WF -> VMC -> {MCMC, LRDMC} for each R value.""" workflows: list[Container] = [] for R in R_VALUES: @@ -266,7 +266,7 @@ def build_pipeline() -> list[Container]: return workflows -# ── Step 2: Exercise Phase-1 diagnostic APIs ───────────────────── +# -- Step 2: Exercise Phase-1 diagnostic APIs --------------------- def run_diagnostics(base_dir: str) -> bool: """Run the new diagnostic / query APIs and verify results. @@ -279,7 +279,7 @@ def run_diagnostics(base_dir: str) -> bool: ok = True - # ── get_all_workflow_statuses ── + # -- get_all_workflow_statuses -- print() print("--- get_all_workflow_statuses ---") statuses = get_all_workflow_statuses(base_dir) @@ -290,11 +290,11 @@ def run_diagnostics(base_dir: str) -> bool: expected = len(R_VALUES) * 5 # wf + vmc + mcmc + lrdmc_t + lrdmc_n per R if len(statuses) < expected: print(f" [WARN] Expected at least {expected} workflows, found {len(statuses)}") - # Not a hard failure — WF doesn't always create state + # Not a hard failure -- WF doesn't always create state else: print(f" [OK] Found {len(statuses)} workflow states") - # ── get_workflow_summary per vmc/mcmc ── + # -- get_workflow_summary per vmc/mcmc -- print() print("--- get_workflow_summary ---") for R in R_VALUES: @@ -312,7 +312,7 @@ def run_diagnostics(base_dir: str) -> bool: else: print(f" {r_dir(R)}/{step}: [no state file]") - # ── helper: dump all dataclass fields ── + # -- helper: dump all dataclass fields -- def _dump_dataclass(label, obj): """Print all fields of a dataclass, truncating stderr_tail.""" d = dataclasses.asdict(obj) @@ -328,7 +328,7 @@ def _dump_dataclass(label, obj): else: print(f" {label}: {d}") - # ── parse_vmc_output ── + # -- parse_vmc_output -- print() print("--- parse_vmc_output ---") for R in R_VALUES: @@ -343,7 +343,7 @@ def _dump_dataclass(label, obj): print(f" [FAIL] No VMC steps parsed for R={R:.2f}") ok = False - # ── parse_mcmc_output ── + # -- parse_mcmc_output -- print() print("--- parse_mcmc_output ---") for R in R_VALUES: @@ -351,7 +351,7 @@ def _dump_dataclass(label, obj): mcmc_data = parse_mcmc_output(mcmc_dir) _dump_dataclass(f"R={R:.2f}", mcmc_data) - # ── parse_lrdmc_output (GFMC_t) ── + # -- parse_lrdmc_output (GFMC_t) -- print() print("--- parse_lrdmc_output (GFMC_t) ---") for R in R_VALUES: @@ -359,7 +359,7 @@ def _dump_dataclass(label, obj): lrdmc_data = parse_lrdmc_output(lrdmc_dir) _dump_dataclass(f"R={R:.2f}", lrdmc_data) - # ── parse_lrdmc_output (GFMC_n) ── + # -- parse_lrdmc_output (GFMC_n) -- print() print("--- parse_lrdmc_output (GFMC_n) ---") for R in R_VALUES: @@ -367,7 +367,7 @@ def _dump_dataclass(label, obj): lrdmc_data = parse_lrdmc_output(lrdmc_dir) _dump_dataclass(f"R={R:.2f}", lrdmc_data) - # ── parse_input_params ── + # -- parse_input_params -- print() print("--- parse_input_params ---") for R in R_VALUES: @@ -385,7 +385,7 @@ def _dump_dataclass(label, obj): return ok -# ── Main ────────────────────────────────────────────────────────── +# -- Main ---------------------------------------------------------- if __name__ == "__main__": base_dir = os.path.dirname(os.path.abspath(__file__)) os.chdir(base_dir) @@ -393,10 +393,10 @@ def _dump_dataclass(label, obj): # 0) pySCF run_pyscf(base_dir) - # 1) Pipeline: WF → VMC → {MCMC, LRDMC} + # 1) Pipeline: WF -> VMC -> {MCMC, LRDMC} print() print("=" * 60) - print(" Step 1: WF → VMC → {MCMC, LRDMC} (local, minimal)") + print(" Step 1: WF -> VMC -> {MCMC, LRDMC} (local, minimal)") print("=" * 60) workflows = build_pipeline() @@ -411,7 +411,7 @@ def _dump_dataclass(label, obj): if all_ok: print(" ALL CHECKS PASSED") else: - print(" SOME CHECKS FAILED — see above for details") + print(" SOME CHECKS FAILED -- see above for details") print("=" * 60) sys.exit(0 if all_ok else 1) diff --git a/jqmc/_checkpoint.py b/jqmc/_checkpoint.py index 6dced2d0..711488b1 100644 --- a/jqmc/_checkpoint.py +++ b/jqmc/_checkpoint.py @@ -8,14 +8,14 @@ File layout:: restart.h5 - ├── _meta/ (format_version, driver_type, mpi_size, ...) - ├── hamiltonian_data/ (shared, saved once) - └── rank_{R}/ (per MPI rank) - ├── driver_config/ - ├── rng_state/ - ├── walker_state/ - ├── observables/ - └── optimizer_state/ (VMCopt only) + |-- _meta/ (format_version, driver_type, mpi_size, ...) + |-- hamiltonian_data/ (shared, saved once) + `-- rank_{R}/ (per MPI rank) + |-- driver_config/ + |-- rng_state/ + |-- walker_state/ + |-- observables/ + `-- optimizer_state/ (VMCopt only) Write flow (mirrors the legacy zip approach):: @@ -167,7 +167,7 @@ def save_rank_checkpoint( optimizer_state: VMCopt optimizer runtime (optional). """ with h5py.File(filepath, "w") as f: - # driver_config — scalars as attrs + # driver_config -- scalars as attrs cfg_grp = f.create_group("driver_config") for k, v in driver_config.items(): if v is None: @@ -454,7 +454,7 @@ def load_driver_config_from_checkpoint( rank: MPI rank whose config to read (default 0). Returns: - Dict of driver-config key→value pairs. + Dict of driver-config key->value pairs. """ config: dict[str, Any] = {} with h5py.File(filepath, "r") as f: diff --git a/jqmc/_jqmc_utility.py b/jqmc/_jqmc_utility.py index c50f40f0..a1276bff 100644 --- a/jqmc/_jqmc_utility.py +++ b/jqmc/_jqmc_utility.py @@ -68,12 +68,12 @@ def _generate_init_electron_configurations( Algorithm: 1. Compute the deterministic atom-assignment templates for spin-up and spin-down electrons by replaying the original state machine **once**. - These templates depend only on (charges, coords) — every walker + These templates depend only on (charges, coords) -- every walker shares them. 2. Tile the templates to shape ``(num_walkers, ned)``. 3. For the only branch in the original that uses per-walker randomness - — Phase 1b "extra up" electrons when - ``tot_num_electron_up > sum(zeta - occup_dn)`` — draw the random + -- Phase 1b "extra up" electrons when + ``tot_num_electron_up > sum(zeta - occup_dn)`` -- draw the random atom indices in one batched ``np.random.randint`` call. 4. Draw all spherical random offsets in one batched call per spin and add them to the chosen atomic coordinates. @@ -148,7 +148,7 @@ def _generate_init_electron_configurations( n_random_extras = 0 # trailing electrons whose owner is random per walker if ned_up <= sum_up_needed: - # Case 1: place exactly into the up_needed slots — fully deterministic. + # Case 1: place exactly into the up_needed slots -- fully deterministic. ptr = 0 for iup in range(ned_up): while True: @@ -267,7 +267,7 @@ def _generate_init_electron_configurations_debug( dn_owner (np.ndarray of shape (num_walkers, tot_num_electron_dn), dtype=int): For each walker `iw` and each down-electron `k`, the atom-index it was assigned to. """ - # Fixed random displacement range (±dst/2 in each coordinate) + # Fixed random displacement range (+/-dst/2 in each coordinate) min_dst = 0.1 max_dst = 1.0 @@ -277,7 +277,7 @@ def _generate_init_electron_configurations_debug( nion = coords.shape[0] zeta = np.array([int(round(c)) for c in charges], dtype=int) - # 2) max_dn_per_atom = floor(zeta[i]/2) for Hund’s rule on down-electrons + # 2) max_dn_per_atom = floor(zeta[i]/2) for Hund's rule on down-electrons max_dn_per_atom = zeta // 2 # 3) Build ion_seq so that each next index is the atom farthest from the previous @@ -308,19 +308,19 @@ def _generate_init_electron_configurations_debug( # 6) Loop over walkers for iw in range(num_walkers): # 6.1) Reset per-walker occupancy - occup_total = np.zeros(nion, dtype=int) # total electrons (↑+↓) on each atom + occup_total = np.zeros(nion, dtype=int) # total electrons (up+dn) on each atom occup_dn = np.zeros(nion, dtype=int) # how many down-electrons on each atom occup_up = np.zeros(nion, dtype=int) # how many up-electrons on each atom cdown = 0 cup = 0 - # 6.2) Compute any “extra” beyond sum(zeta) + # 6.2) Compute any "extra" beyond sum(zeta) nel = tot_num_electron_up + tot_num_electron_dn ztot = int(np.sum(zeta)) nelupeff = nel - ztot if nel > ztot else 0 # ----------------------------------------- - # Phase 1a: Place all down-electrons under Hund’s limit first + # Phase 1a: Place all down-electrons under Hund's limit first # ----------------------------------------- ned_dn = tot_num_electron_dn down_positions = np.zeros((ned_dn, 3), dtype=dtype_np) @@ -346,7 +346,7 @@ def _generate_init_electron_configurations_debug( cond = occup_total[atom] < zeta[atom] if cond: - # Place one ↓-electron around coords[atom] + random_offset + # Place one dn-electron around coords[atom] + random_offset x0, y0, z0 = coords[atom] distance = np.random.uniform(min_dst, max_dst) theta = np.random.uniform(0, np.pi) @@ -382,7 +382,7 @@ def _generate_init_electron_configurations_debug( j_counter += 1 # ----------------------------------------- - # Phase 1b: Place up-electrons exactly to “fill to zeta” if possible + # Phase 1b: Place up-electrons exactly to "fill to zeta" if possible # ----------------------------------------- # Compute how many up each atom needs to reach zeta: up_needed = zeta - occup_dn # array of length nion @@ -391,7 +391,7 @@ def _generate_init_electron_configurations_debug( ned_up = tot_num_electron_up up_positions = np.zeros((ned_up, 3), dtype=dtype_np) - # Case 1: ned_up <= sum_up_needed → place ned_up among those up_needed slots + # Case 1: ned_up <= sum_up_needed -> place ned_up among those up_needed slots if ned_up <= sum_up_needed: ptr = 0 for iup in range(ned_up): @@ -399,7 +399,7 @@ def _generate_init_electron_configurations_debug( while not placed: atom = ion_seq[ptr % nion] if occup_up[atom] < up_needed[atom]: - # Place one ↑-electron here + # Place one up-electron here x0, y0, z0 = coords[atom] distance = np.random.uniform(min_dst, max_dst) theta = np.random.uniform(0, np.pi) @@ -433,9 +433,9 @@ def _generate_init_electron_configurations_debug( placed = True ptr += 1 - # Case 2: ned_up > sum_up_needed → give each atom its up_needed, then place extras + # Case 2: ned_up > sum_up_needed -> give each atom its up_needed, then place extras else: - # (a) first satisfy every atom’s up_needed + # (a) first satisfy every atom's up_needed cnt = 0 for atom in ion_seq: to_give = int(up_needed[atom]) @@ -471,7 +471,7 @@ def _generate_init_electron_configurations_debug( cnt += 1 cup += 1 - # (b) now place the “extra” up = ned_up - sum_up_needed on any atom (fallback) + # (b) now place the "extra" up = ned_up - sum_up_needed on any atom (fallback) extra_up = ned_up - sum_up_needed for _ in range(extra_up): idx = int(np.floor(np.random.rand() * nion)) @@ -508,11 +508,11 @@ def _generate_init_electron_configurations_debug( cup += 1 # ----------------------------------------- - # Phase 2: If “extra” electrons remain (nelupeff > 0), place them now. + # Phase 2: If "extra" electrons remain (nelupeff > 0), place them now. # (This is almost never needed if tot_up+tot_dn == sum(zeta), but we include it for completeness.) # ----------------------------------------- if nelupeff > 1: - # 2a) extra down beyond Hund’s limit + # 2a) extra down beyond Hund's limit sum_dn_assigned = int(np.sum(occup_dn)) extra_dn = ned_dn - sum_dn_assigned for _ in range(extra_dn): @@ -567,7 +567,7 @@ def _generate_init_electron_configurations_debug( @lru_cache(maxsize=None) def _cart_to_spherical_matrix(l: int) -> np.ndarray: - """Precomputed cart -> real-spherical transform for angular momentum ``l`` (0–6). + """Precomputed cart -> real-spherical transform for angular momentum ``l`` (0-6). The matrix has shape ``((l+1)(l+2)/2, 2l+1)`` and satisfies ``A_sph = A_cart @ T`` under the normalization used in the codebase. Values diff --git a/jqmc/_precision.py b/jqmc/_precision.py index edf28d20..78839e3a 100644 --- a/jqmc/_precision.py +++ b/jqmc/_precision.py @@ -9,60 +9,60 @@ a violation of 3a or 3b. ------------------------------------------------------------ -Principle 1 — One Precision Zone is owned by exactly one module. +Principle 1 -- One Precision Zone is owned by exactly one module. ------------------------------------------------------------ A zone (e.g. ``ao_eval``, ``coulomb``) is *defined and consumed* in a single -module. The mapping zone ↔ owning module is one-to-one and is documented in +module. The mapping zone <-> owning module is one-to-one and is documented in the table below (and enforced by convention in ``_FULL_PRECISION`` / ``_MIXED_PRECISION``). ------------------------------------------------------------ -Principle 2 — A module may own multiple Precision Zones. +Principle 2 -- A module may own multiple Precision Zones. ------------------------------------------------------------ Different code paths in the same module legitimately need different precisions (e.g. ``ao_eval`` vs ``ao_grad_lap``, or ``det_eval`` vs ``det_ratio``). Each zone is named for its *purpose*, not for its dtype. ------------------------------------------------------------ -Principle 3 — Cast responsibility lies with the function that does +Principle 3 -- Cast responsibility lies with the function that does arithmetic on the value, never with passthrough wrappers. ------------------------------------------------------------ Definition. *arithmetic* means consuming a value as an operand of a numerical operation (``+ - * /``, ``jnp.linalg.norm``, ``jnp.dot``, ``@``, ``jnp.exp``, -…) **or** as an input to ``jax.grad`` / ``jax.jacrev`` / ``jax.hessian``. +...) **or** as an input to ``jax.grad`` / ``jax.jacrev`` / ``jax.hessian``. Operations that do *not* count as arithmetic and therefore do *not* trigger a cast: ``len(x)``, ``x.shape``, ``x[i]`` (index lookup), the *target* of ``.at[i].set(y)``, and forwarding ``x`` as an argument to another function. -Principle 3a — Arguments are frozen. +Principle 3a -- Arguments are frozen. Function arguments are treated as **frozen**, in the same sense as the attributes of a ``@dataclass(frozen=True)``: the name introduced by the parameter list **must not be rebound** for the entire body of the function. In particular, ``arg = jnp.asarray(arg, dtype=...)`` at the - top of a function is forbidden — it silently coerces the argument for + top of a function is forbidden -- it silently coerces the argument for every later use, including forwarding. - Consequences (not extra rules — direct corollaries of "frozen"): + Consequences (not extra rules -- direct corollaries of "frozen"): * Forwarding neutrality. A value forwarded to a callee transits in the dtype it was received in; the callee is responsible for casting it to *its* own zone (Principle 3b). * Cast at the use site. When the function consumes ``arg`` as an operand of its own arithmetic, the cast appears **inside the expression** (``arg.astype(dtype)``). Do *not* preemptively - introduce a local alias just to hold the cast — only do so when + introduce a local alias just to hold the cast -- only do so when the cast result is reused multiple times, in which case introduce a *new* local variable with a different name (e.g. ``arg_local = arg.astype(dtype)``). The original ``arg`` always remains frozen. -Principle 3b — Local cast at the point of arithmetic. +Principle 3b -- Local cast at the point of arithmetic. A function casts a value to its own zone's dtype **immediately before** consuming it as an operand. Inputs and outputs of the function's arithmetic both live in its zone. Intermediate computations may use a higher precision when needed for numerical reasons (the canonical case being ``r - R``: reconstruct the difference in the **dtype the value - was received in** — i.e. the precision chosen by the upper layer — + was received in** -- i.e. the precision chosen by the upper layer -- to avoid catastrophic cancellation, then down-cast the result back to the function's own zone). In jQMC the upstream (mcmc walker state) is always fp64, so in practice the reconstruction happens in fp64; the @@ -72,7 +72,7 @@ name when the incoming value's own dtype already carries the right precision. -Worked example (the ECP → AO bug this design prevents):: +Worked example (the ECP -> AO bug this design prevents):: # WRONG: rebinding `r_carts` at the top of compute_coulomb forwards a # fp32-truncated array to compute_AOs, even though `ao_eval` is fp64. @@ -86,7 +86,7 @@ def compute_coulomb(r_carts, R_carts): # RIGHT: forwarding stays in the caller's dtype; the local arithmetic # reconstructs the difference in the dtype the values were received in - # (the upper-layer precision — fp64 in jQMC because mcmc walker state + # (the upper-layer precision -- fp64 in jQMC because mcmc walker state # is fp64) and casts the result back to the function's own zone. def compute_coulomb(r_carts, R_carts): ao = compute_AOs(..., r_carts, R_carts) # 3a: forward as-is @@ -98,7 +98,7 @@ def compute_coulomb(r_carts, R_carts): Auditing recipe. To verify a module: * (3a) Search for ``arg = jnp.asarray(arg, dtype=...)`` at the top of - any public function. Each occurrence is a 3a candidate violation — + any public function. Each occurrence is a 3a candidate violation -- the rebind silently coerces the argument for any subsequent forwarding too. * (3b) For each arithmetic expression, check that all operands have @@ -132,7 +132,7 @@ def compute_coulomb(r_carts, R_carts): * Basis-data storage accessors. ``_*_jnp`` properties on selectable-precision dataclasses whose underlying storage field is typed ``npt.NDArray[np.float64]`` are *lift-only* - adapters (numpy → ``jax.Array``), not arithmetic. The dtype + adapters (numpy -> ``jax.Array``), not arithmetic. The dtype is fp64 by construction: storage is loaded from HDF5/TREXIO/optimizer output (see Phase A1 numpy-storage migration), and downcasting at the accessor would silently @@ -158,8 +158,8 @@ def compute_coulomb(r_carts, R_carts): Users choose one of two modes: -- ``"full"`` (default) — all zones float64 (backward compatible). -- ``"mixed"`` — recommended mixed precision; low-risk zones become +- ``"full"`` (default) -- all zones float64 (backward compatible). +- ``"mixed"`` -- recommended mixed precision; low-risk zones become float32 while numerically sensitive zones stay float64. Individual zone assignments are **not** user-configurable; they are @@ -174,18 +174,18 @@ def compute_coulomb(r_carts, R_carts): Zone Owning module Default Mixed risk E_L path ================== ================================= ========= ======== ===== ========= ``ao_eval`` atomic_orbital.py (forward) float64 float32 low core -``ao_grad_lap`` atomic_orbital.py (grad/Lap) float64 float64 high§ core +``ao_grad_lap`` atomic_orbital.py (grad/Lap) float64 float64 highSection core ``mo_eval`` molecular_orbital.py (forward) float64 float64 high* core ``mo_grad`` molecular_orbital.py (gradient) float64 float64 high core ``mo_lap`` molecular_orbital.py (Laplacian) float64 float64 high core -``jastrow_eval`` jastrow_factor.py (forward) float64 float32 low core† +``jastrow_eval`` jastrow_factor.py (forward) float64 float32 low core* ``jastrow_grad_lap`` jastrow_factor.py (grad/lap) float64 float32 low core -``jastrow_ratio`` jastrow_factor.py (ratio update) float64 float32 low indirect‡ +``jastrow_ratio`` jastrow_factor.py (ratio update) float64 float32 low indirect** ``det_eval`` determinant.py (geminal + log-det) float64 float64 high core ``det_grad_lap`` determinant.py (grad/lap of lnDet) float64 float64 high core -``det_ratio`` determinant.py (SM ratio update) float64 float64 high indirect‡ +``det_ratio`` determinant.py (SM ratio update) float64 float64 high indirect** ``coulomb`` coulomb_potential.py float64 float32 low-med core -``wf_eval`` wavefunction.py (Psi, ln Psi) float64 float64 high core† +``wf_eval`` wavefunction.py (Psi, ln Psi) float64 float64 high core* ``wf_kinetic`` wavefunction.py (T_L assembly) float64 float64 high core ``wf_ratio`` wavefunction.py (Psi(R')/Psi(R)) float64 float64 high no ``local_energy`` hamiltonians.py (T + V assembly) float64 float64 high core @@ -197,24 +197,24 @@ def compute_coulomb(r_carts, R_carts): its output feeds the determinant matrix, where fp32 round-off is amplified by log|det|. -† ``jastrow_eval`` and ``wf_eval`` are on the E_L core path but their +* ``jastrow_eval`` and ``wf_eval`` are on the E_L core path but their forward values (J and ln|Psi|) do not enter the E_L formula directly (E_L depends on *derivatives* of ln|Psi|), so fp32 in these zones alone has no E_L impact. -§ ``ao_grad_lap`` is fp64 even in mixed mode because the analytic +Section ``ao_grad_lap`` is fp64 even in mixed mode because the analytic Laplacian kernel for spherical AOs contains catastrophic cancellation -(``4 Z² r² − 6 Z`` and ``(safe_div − 2 Z·base)² − safe_div² − 2 Z`` +(``4 Z^2 r^2 - 6 Z`` and ``(safe_div - 2 Z*base)^2 - safe_div^2 - 2 Z`` terms) that fp32 cannot resolve for tight Gaussians. ``ao_grad`` is merged with ``ao_lap`` into this single zone because the fused ``compute_AOs_value_grad_lap`` kernel shares one heavy expression (``exp / pow / phi / S_l_m``) across grad and lap. Running that shared kernel at fp32 would break the lap path, so the unified zone is fp64 -always — a small extra cost on the standalone ``compute_AOs_grad`` +always -- a small extra cost on the standalone ``compute_AOs_grad`` (which is not on the per-step hot path) in exchange for a single source of truth for the shared kernel dtype. -‡ ``det_ratio`` and ``jastrow_ratio`` affect E_L **indirectly** through +** ``det_ratio`` and ``jastrow_ratio`` affect E_L **indirectly** through the ECP non-local potential, which evaluates Psi(R')/Psi(R) on a quadrature grid via rank-1 ratio updates (see ``coulomb_potential.compute_ecp_non_local_parts_nearest_neighbors_fast_update``). @@ -236,8 +236,8 @@ def _compute_AOs_kernel(aos_data, r_carts): # the atomic centers are loaded from disk as fp64). The result # is then down-cast to this function's own zone (``ao_eval``). # NOTE: never reach for another module's zone (e.g. - # ``get_dtype_jnp("local_energy")``) here — that violates - # Principle 1 (zone ↔ owning module is 1:1). atomic_orbital.py + # ``get_dtype_jnp("local_energy")``) here -- that violates + # Principle 1 (zone <-> owning module is 1:1). atomic_orbital.py # may only consult ao_eval / ao_grad_lap. dtype_jnp = get_dtype_jnp("ao_eval") R_carts = aos_data._atomic_center_carts_jnp @@ -336,8 +336,8 @@ def _compute_AOs_kernel(aos_data, r_carts): # # ao_grad_lap - analytic gradient + Laplacian kernel for spherical/ # Cartesian AOs. Lap arithmetic contains catastrophic -# cancellation (``4 Z² r² − 6 Z`` and -# ``(safe_div − 2 Z·base)² − safe_div² − 2 Z``). +# cancellation (``4 Z^2 r^2 - 6 Z`` and +# ``(safe_div - 2 Z*base)^2 - safe_div^2 - 2 Z``). # fp64 mandatory. This zone merges grad and lap # because the fused ``compute_AOs_value_grad_lap`` # kernel evaluates ``exp / pow / phi / S_l_m`` once @@ -369,8 +369,8 @@ def _compute_AOs_kernel(aos_data, r_carts): _MIXED_PRECISION: dict[str, str] = { # atomic_orbital.py "ao_eval": "float32", # low risk (heavy kernel) - "ao_grad_lap": "float64", # high risk (catastrophic cancellation in 4Z²r²-6Z terms; - # unified zone — grad and lap share the fused compute_AOs_value_grad_lap kernel, + "ao_grad_lap": "float64", # high risk (catastrophic cancellation in 4Z^2r^2-6Z terms; + # unified zone -- grad and lap share the fused compute_AOs_value_grad_lap kernel, # which must run at fp64 to protect the lap path) # molecular_orbital.py "mo_eval": "float64", # high risk (feeds det_eval) @@ -407,7 +407,7 @@ def _compute_AOs_kernel(aos_data, r_carts): # state within a Python process: ``configure(mode)`` clears and refills it, # and ``get_dtype_jnp`` / ``get_dtype_np`` read from it. No other variable # (class attribute, environment variable, etc.) holds the active dtype -# mapping — this dict is the only place to consult or mutate. +# mapping -- this dict is the only place to consult or mutate. _zone_dtypes: dict[str, str] = {} @@ -455,7 +455,7 @@ def _set_zone(zone: str, dtype_str: str) -> None: """Override a single zone's dtype at runtime (developer use only). Must be called **after** :func:`configure`. This is intentionally - private — normal users select ``"full"`` or ``"mixed"`` mode and the + private -- normal users select ``"full"`` or ``"mixed"`` mode and the per-zone mapping is determined by ``_FULL_PRECISION`` / ``_MIXED_PRECISION``. diff --git a/jqmc/_setting.py b/jqmc/_setting.py index 2f3057e0..8745e3cf 100644 --- a/jqmc/_setting.py +++ b/jqmc/_setting.py @@ -80,10 +80,10 @@ # zone's current dtype and returns ``(atol, rtol)``. # # Levels: -# strict — two exact implementations of the same quantity (debug vs +# strict -- two exact implementations of the same quantity (debug vs # production, analytic vs autodiff). Difference is pure # floating-point round-off. -# loose — comparison involving numerical differentiation or quadrature. +# loose -- comparison involving numerical differentiation or quadrature. # Finite-difference truncation error dominates, so tolerances # are much wider. _TOLERANCE: dict[str, dict[str, tuple[float, float]]] = { @@ -98,8 +98,8 @@ # appropriate value for the current precision zone. # # Constants: -# machine_precision — floor for safe ratio in diagnostics. -# rcond_svd — threshold for SVD pseudoinverse of the geminal matrix. +# machine_precision -- floor for safe ratio in diagnostics. +# rcond_svd -- threshold for SVD pseudoinverse of the geminal matrix. _EPS_DTYPE_AWARE: dict[str, dict[str, float]] = { "machine_precision": {"float64": 1e-38, "float32": 1e-38}, "rcond_svd": {"float64": 1e-20, "float32": 1e-16}, diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index e194cf3f..b1d6bb7e 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -5,12 +5,12 @@ Precision Zones: - ``ao_eval``: forward AO evaluation (compute_AOs and internal helpers). - ``ao_grad_lap``: AO gradient and Laplacian (compute_AOs_grad, - compute_AOs_laplacian). Pinned to fp64 even in mixed mode — the + compute_AOs_laplacian). Pinned to fp64 even in mixed mode -- the shared kernel must avoid catastrophic cancellation in the Laplacian arithmetic (e.g. ``4 Z^2 r^2 - 6 Z`` for s-type AOs). The fused :func:`compute_AOs_value_grad_lap` API returns ``(val, gx, gy, -gz, lap)`` from a single dispatch — the heavy block (``exp``, polynomial +gz, lap)`` from a single dispatch -- the heavy block (``exp``, polynomial chain, ``S_l_m``) is shared across val/grad/lap instead of recomputed three times. ``val`` is downcast to ``ao_eval`` while grad/lap stay in ``ao_grad_lap``. Use it when value, gradient, and Laplacian are all @@ -573,7 +573,7 @@ def _prim_groups_by_K(self) -> tuple: Used by :func:`_reduce_primitives_to_aos` to replace ``segment_sum`` (which falls back to a scatter-add while-loop in XLA) with a small, fixed number of dense ``reduce_sum`` ops - — one per unique contraction depth K. + -- one per unique contraction depth K. Returns: tuple of ``(K, ao_idx_np, prim_idx_np, is_identity_perm)``. @@ -810,7 +810,7 @@ class AOs_sphe_data: atomic_labels=["H1", "H2"], ) - # Per atom: 14 AOs (3 S, 2×P shells -> 6, 1×D shell -> 5); 18 primitives. + # Per atom: 14 AOs (3 S, 2xP shells -> 6, 1xD shell -> 5); 18 primitives. # Two atoms -> num_ao=28, num_ao_prim=36. exponents = [ # atom 1 @@ -1024,7 +1024,7 @@ def sanity_check(self) -> None: c_infos.append(info) break else: - # no matching cluster → create new + # no matching cluster -> create new clusters.append([exp, coef, l, [info]]) # --- validate each cluster --- @@ -1037,7 +1037,7 @@ def sanity_check(self) -> None: if len(c_infos) != expected_count: logger.error( f"[nucleus={nucleus}] " - f"(exp≈{c_exp:.5g}, coef≈{c_coef:.5g}, l={l}): " + f"(exp~={c_exp:.5g}, coef~={c_coef:.5g}, l={l}): " f"found {len(c_infos)} entries, expected {expected_count}" ) raise ValueError(f"Spherical completeness count failed for nucleus {nucleus}") @@ -1048,7 +1048,7 @@ def sanity_check(self) -> None: if missing or extra: logger.error( f"[nucleus={nucleus}] " - f"(exp≈{c_exp:.5g}, coef≈{c_coef:.5g}, l={l}):\n" + f"(exp~={c_exp:.5g}, coef~={c_coef:.5g}, l={l}):\n" f" missing m-values: {sorted(missing)}\n" f" unexpected m-values:{sorted(extra)}" ) @@ -1210,7 +1210,7 @@ def _prim_groups_by_K(self) -> tuple: Used by :func:`_reduce_primitives_to_aos` to replace ``segment_sum`` (which falls back to a scatter-add while-loop in XLA) with a small, fixed number of dense ``reduce_sum`` ops - — one per unique contraction depth K. + -- one per unique contraction depth K. Returns: tuple of ``(K, ao_idx_np, prim_idx_np, is_identity_perm)``. @@ -2124,7 +2124,7 @@ def _reduce_primitives_to_aos(values: jax.Array, aos_data) -> jax.Array: return summed return summed[jnp.asarray(inv_perm_np)] - # General path: per-K dense reduce → concat in bucket order → final + # General path: per-K dense reduce -> concat in bucket order -> final # inverse-permutation gather. pieces = [] for _K, prim_idx_np in groups: @@ -2178,7 +2178,7 @@ def _int_pow_unrolled_cart(base: jax.Array, exp_arr: jax.Array, L_MAX: int) -> j the power into a single fused elementwise kernel. Numerically equivalent to - ``jnp.where(exp == 0, 1.0, base ** exp_b)`` for ``exp ∈ [0, L_MAX]`` + ``jnp.where(exp == 0, 1.0, base ** exp_b)`` for ``exp in [0, L_MAX]`` (bitwise-identical: same left-to-right multiplication tree). """ # Broadcast exp_arr against base: prepend trailing singleton axes. @@ -2220,7 +2220,7 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A dtype_jnp = get_dtype_jnp("ao_eval") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) # via JAX promotion when one operand is fp64, then downcast to the ao_eval - # zone (Principle 3b — local cast at point of arithmetic). r_carts is + # zone (Principle 3b -- local cast at point of arithmetic). r_carts is # forwarded as-is (Principle 3a) and R_carts is read from the fp64 storage # accessor on the basis-data dataclass. R_carts = aos_data._atomic_center_carts_prim_jnp @@ -2240,13 +2240,13 @@ def _compute_AOs_cart(aos_data: AOs_cart_data, r_carts: jnpt.ArrayLike) -> jax.A r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) - # Radial part at primitive level → reduce to AO level (case 1: see docstring). + # Radial part at primitive level -> reduce to AO level (case 1: see docstring). NR_dup = N_n_dup[:, None] * R_n_dup # (num_ao_prim, n_elec) NR_ao = _reduce_primitives_to_aos(NR_dup, aos_data) # (num_ao, n_elec) # AO-level coordinates: each AO sits on exactly one atom, so we use the - # AO→atom mapping (``_atomic_center_carts_jnp``, length num_ao) rather than - # the prim→atom mapping (``_atomic_center_carts_prim_jnp``, length num_ao_prim). + # AO->atom mapping (``_atomic_center_carts_jnp``, length num_ao) rather than + # the prim->atom mapping (``_atomic_center_carts_prim_jnp``, length num_ao_prim). R_carts_ao = aos_data._atomic_center_carts_jnp r_R_diffs_ao = (r_carts[None, :, :] - R_carts_ao[:, None, :]).astype(dtype_jnp) x_ao, y_ao, z_ao = r_R_diffs_ao[..., 0], r_R_diffs_ao[..., 1], r_R_diffs_ao[..., 2] @@ -2284,7 +2284,7 @@ def _compute_AOs_sphe(aos_data: AOs_sphe_data, r_carts: jnpt.ArrayLike) -> jax.A dtype_jnp = get_dtype_jnp("ao_eval") # Reconstruct r-R in caller-supplied precision (fp64 from MCMC walker state) # via JAX promotion when one operand is fp64, then downcast to the ao_eval - # zone (Principle 3b — local cast at point of arithmetic). r_carts is + # zone (Principle 3b -- local cast at point of arithmetic). r_carts is # forwarded as-is (Principle 3a) and R_carts is read from the fp64 storage # accessor on the basis-data dataclass. R_carts = aos_data._atomic_center_carts_prim_jnp @@ -2308,7 +2308,7 @@ def _compute_AOs_sphe(aos_data: AOs_sphe_data, r_carts: jnpt.ArrayLike) -> jax.A r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) - # Radial part at primitive level → reduce to AO level. Same distributive-law + # Radial part at primitive level -> reduce to AO level. Same distributive-law # rationale as in :func:`_compute_AOs_cart`: the angular factor # :math:`Y_{lm}` is constant across primitives of a given AO, so we can # reduce ``N R`` first and then multiply by the AO-level ``S_{lm}``. @@ -2579,7 +2579,7 @@ def _compute_S_l_m_and_grad_lap(r_R_diffs_uq: jnp.ndarray) -> tuple[jax.Array, j consumer site. Running the helper at fp64 is mandated by the catastrophic cancellation in the laplacian arithmetic (``4 Z^2 r^2 - 6 Z``); the solid-harmonics polynomial expansion is - cheap (49 × num_R × num_e) compared to the contracted AO formulas, + cheap (49 x num_R x num_e) compared to the contracted AO formulas, so the cost of fp64 here is small. Returns: @@ -2836,7 +2836,7 @@ def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.n Implementation note (perf, poly-after-reduce): Mirrors the rewrite in ``_compute_AOs_value_grad_lap_cart``. - Three radial moments ``NR``, ``ZNR``, ``Z²NR`` are reduced from + Three radial moments ``NR``, ``ZNR``, ``Z^2NR`` are reduced from primitive rank to AO rank, then .. math:: @@ -2870,7 +2870,7 @@ def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.n r2_prim = jnp.sum(diff**2, axis=-1) pref_prim = N[:, None] * c[:, None] * jnp.exp(-Z[:, None] * r2_prim) - # Three radial moments contracted at primitive rank → reduced to AO rank. + # Three radial moments contracted at primitive rank -> reduced to AO rank. Z_b = Z[:, None] NR_ao = _reduce_primitives_to_aos(pref_prim, aos_data) ZNR_ao = _reduce_primitives_to_aos(Z_b * pref_prim, aos_data) @@ -2888,7 +2888,7 @@ def _compute_AOs_laplacian_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.n # Static-unrolled integer powers at AO rank. See grad analog above for # rationale of the shifted-exponent formulation; here we additionally - # need ``∂²_x x^n = n(n-1) x^(n-2)``. + # need ``d/d^2_x x^n = n(n-1) x^(n-2)``. px = _int_pow_unrolled_cart(x, nx_ao, L_MAX) py = _int_pow_unrolled_cart(y, ny_ao, L_MAX) pz = _int_pow_unrolled_cart(z, nz_ao, L_MAX) @@ -3100,7 +3100,7 @@ def _compute_AOs_laplacian_autodiff(aos_data: AOs_sphe_data | AOs_cart_data, r_c See compute_AOs_laplacian_api """ - # Forward r_carts as-is (Principle 3a — no parameter rebind). compute_AOs's + # Forward r_carts as-is (Principle 3a -- no parameter rebind). compute_AOs's # inner kernels reconstruct r-R in caller-supplied precision and downcast to # the ao_eval zone at the use site; the hessian inherits that dtype. # not very fast, but it works. @@ -3179,8 +3179,8 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra Implementation note (perf, poly-after-reduce): Mirrors the rewrite in ``_compute_AOs_value_grad_lap_cart``. - Two radial moments ``NR = Σ_p N_p c_p e^{-Z_p r^2}`` and - ``ZNR = Σ_p Z_p N_p c_p e^{-Z_p r^2}`` are reduced from + Two radial moments ``NR = Sum_p N_p c_p e^{-Z_p r^2}`` and + ``ZNR = Sum_p Z_p N_p c_p e^{-Z_p r^2}`` are reduced from primitive rank to AO rank, then .. math:: @@ -3212,10 +3212,10 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra r2_prim = jnp.sum(diff**2, axis=-1) pref_prim = N[:, None] * c[:, None] * jnp.exp(-Z[:, None] * r2_prim) - # Two radial moments contracted at primitive rank → reduced to AO rank. + # Two radial moments contracted at primitive rank -> reduced to AO rank. Z_b = Z[:, None] - NR_ao = _reduce_primitives_to_aos(pref_prim, aos_data) # Σ_p pref_p - ZNR_ao = _reduce_primitives_to_aos(Z_b * pref_prim, aos_data) # Σ_p Z_p pref_p + NR_ao = _reduce_primitives_to_aos(pref_prim, aos_data) # Sum_p pref_p + ZNR_ao = _reduce_primitives_to_aos(Z_b * pref_prim, aos_data) # Sum_p Z_p pref_p # AO-level coordinates: each AO sits on exactly one atom. R_carts_ao = aos_data._atomic_center_carts_jnp @@ -3229,7 +3229,7 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra # Static-unrolled integer powers at AO rank. Shifted exponent # ``max(n - 1, 0)`` combined with prefactor ``n`` expresses - # ``∂_x x^n = n x^(n-1)`` directly; for ``n == 0`` the prefactor zeros + # ``d/d_x x^n = n x^(n-1)`` directly; for ``n == 0`` the prefactor zeros # the term, matching the analytic limit (no eps, no divisions). px = _int_pow_unrolled_cart(x, nx_ao, L_MAX) py = _int_pow_unrolled_cart(y, ny_ao, L_MAX) @@ -3244,7 +3244,7 @@ def _compute_AOs_grad_analytic_cart(aos_data: AOs_cart_data, r_carts: jnp.ndarra P = px * py * pz # (num_ao, n_elec) - # ∂_a φ = NR · ∂_a P − 2 x_a · ZNR · P + # d/d_a phi = NR * d/d_a P - 2 x_a * ZNR * P gx = NR_ao * ((nx_b * qpx) * py * pz) - 2.0 * x * ZNR_ao * P gy = NR_ao * (px * (ny_b * qpy) * pz) - 2.0 * y * ZNR_ao * P gz = NR_ao * (px * py * (nz_b * qpz)) - 2.0 * z * ZNR_ao * P @@ -3286,9 +3286,9 @@ def _compute_AOs_grad_analytic_sphe(aos_data: AOs_sphe_data, r_carts: jnp.ndarra # Use a single ``_compute_S_l_m_and_grad_lap`` call and reuse its value # output instead of re-running ``_compute_S_l_m``. The helper is pinned # to the ``ao_grad_lap`` zone (fp64), and this caller is also in the - # ``ao_grad_lap`` zone after PR1-A — so the ``.astype(dtype_jnp)`` calls + # ``ao_grad_lap`` zone after PR1-A -- so the ``.astype(dtype_jnp)`` calls # below are no-ops at runtime but kept for explicit Principle 3b - # documentation. Lap output is unused — JAX DCE eliminates it because + # documentation. Lap output is unused -- JAX DCE eliminates it because # this whole function is inlined inside the caller's @jit and the lap # branch never reaches a sink. max_ml = 49 @@ -3377,7 +3377,7 @@ def _compute_AOs_value_grad_lap_cart( Z^2NR = \\sum_p Z_p^2\\, \\mathrm{pref}_p, all primitive-rank arrays disappear after three radial reductions - — value, grad and Laplacian then reduce to AO-rank polynomial + -- value, grad and Laplacian then reduce to AO-rank polynomial algebra: .. math:: @@ -3388,7 +3388,7 @@ def _compute_AOs_value_grad_lap_cart( + (4 r^2\\, Z^2NR - 6\\, ZNR) \\cdot P The kernel performs one prim-rank reduce per radial moment - (NR / ZNR / Z²NR) and runs all polynomial work at AO rank. + (NR / ZNR / Z^2NR) and runs all polynomial work at AO rank. This avoids the four ``(n_walker, num_ao_prim, n_elec)`` prim-rank tuples (``phi``, ``Kx``, ``Ky``, ``Kz``) that would otherwise dominate the kinetic energy / local energy DRAM @@ -3415,19 +3415,19 @@ def _compute_AOs_value_grad_lap_cart( r2_prim = jnp.sum(diff**2, axis=-1) pref_prim = N[:, None] * c[:, None] * jnp.exp(-Z[:, None] * r2_prim) - # Three radial moments contracted at primitive rank → reduced to AO rank + # Three radial moments contracted at primitive rank -> reduced to AO rank # via a single _reduce_primitives_to_aos each. Everything downstream # operates at AO rank (num_ao << num_ao_prim), eliminating the # (n_walker, num_ao_prim, n_elec)-sized intermediate tuple that # dominated the kinetic energy HLO. Z_b = Z[:, None] - NR_ao = _reduce_primitives_to_aos(pref_prim, aos_data) # Σ_p pref_p - ZNR_ao = _reduce_primitives_to_aos(Z_b * pref_prim, aos_data) # Σ_p Z_p pref_p - Z2NR_ao = _reduce_primitives_to_aos(Z_b * Z_b * pref_prim, aos_data) # Σ_p Z_p^2 pref_p + NR_ao = _reduce_primitives_to_aos(pref_prim, aos_data) # Sum_p pref_p + ZNR_ao = _reduce_primitives_to_aos(Z_b * pref_prim, aos_data) # Sum_p Z_p pref_p + Z2NR_ao = _reduce_primitives_to_aos(Z_b * Z_b * pref_prim, aos_data) # Sum_p Z_p^2 pref_p # AO-level coordinates: each AO sits on exactly one atom, so we use the - # AO→atom mapping (length num_ao). r-R is reconstructed in fp64 then - # cast to the ao_grad_lap zone — same protocol as the prim-rank diff + # AO->atom mapping (length num_ao). r-R is reconstructed in fp64 then + # cast to the ao_grad_lap zone -- same protocol as the prim-rank diff # above, ensuring bit-exact match between the two coordinate sources # whenever an electron sits on a nucleus. R_carts_ao = aos_data._atomic_center_carts_jnp @@ -3465,19 +3465,19 @@ def _compute_AOs_value_grad_lap_cart( # value finalize: only downcast site (Principle 3b). val = (NR_ao * P).astype(dtype_eval) - # grad finalize (kept in ao_grad_lap zone — no cast). - # ∂_a φ = NR · ∂_a P − 2 x_a · ZNR · P, with - # ∂_x P = n_x x^{n_x-1} y^{n_y} z^{n_z}, etc. + # grad finalize (kept in ao_grad_lap zone -- no cast). + # d/d_a phi = NR * d/d_a P - 2 x_a * ZNR * P, with + # d/d_x P = n_x x^{n_x-1} y^{n_y} z^{n_z}, etc. gx = NR_ao * ((nx_b * qpx) * py * pz) - 2.0 * x * ZNR_ao * P gy = NR_ao * (px * (ny_b * qpy) * pz) - 2.0 * y * ZNR_ao * P gz = NR_ao * (px * py * (nz_b * qpz)) - 2.0 * z * ZNR_ao * P - # lap finalize (kept in ao_grad_lap zone — no cast). - # ∇²P = Σ_a n_a(n_a-1) x_a^{n_a-2} Π_{b≠a} x_b^{n_b} + # lap finalize (kept in ao_grad_lap zone -- no cast). + # nabla^2P = Sum_a n_a(n_a-1) x_a^{n_a-2} Prod_{b!=a} x_b^{n_b} lapP = ( (nx_b * (nx_b - 1.0) * qppx) * py * pz + px * (ny_b * (ny_b - 1.0) * qppy) * pz + px * py * (nz_b * (nz_b - 1.0) * qppz) ) - # x·∂_xP + y·∂_yP + z·∂_zP = (n_x + n_y + n_z) · P (Euler identity); + # x*d/d_xP + y*d/d_yP + z*d/d_zP = (n_x + n_y + n_z) * P (Euler identity); # but keep the explicit form for bit-exact match with the legacy # prim-rank rewrite when arithmetic order matters. rdotgradP = x * (nx_b * qpx) * py * pz + y * px * (ny_b * qpy) * pz + z * px * py * (nz_b * qpz) @@ -3528,7 +3528,7 @@ def _compute_AOs_value_grad_lap_sphe( r_squared = jnp.sum(r_R_diffs**2, axis=-1) R_n_dup = c_jnp[:, None] * jnp.exp(-Z_jnp[:, None] * r_squared) - # Single S_l_m call returning (vals, grads, laps) — replaces the + # Single S_l_m call returning (vals, grads, laps) -- replaces the # 2-3x duplicate evaluations across the legacy eval/grad/lap kernels. S_l_m_vals_all, S_l_m_grads_all, S_l_m_laps_all = _compute_S_l_m_and_grad_lap(r_R_diffs_uq) max_ml = S_l_m_vals_all.shape[0] @@ -3550,7 +3550,7 @@ def _compute_AOs_value_grad_lap_sphe( # value finalize: only downcast site (Principle 3b). val = _reduce_primitives_to_aos(AOs_dup.astype(dtype_eval), aos_data) - # grad finalize (kept in ao_grad_lap zone — no cast). + # grad finalize (kept in ao_grad_lap zone -- no cast). grad_from_R = AOs_dup[..., None] * (-2.0 * Z_jnp[:, None, None] * r_R_diffs) grad_from_S = pref[..., None] * S_l_m_grad_dup grad_dup = grad_from_R + grad_from_S @@ -3558,7 +3558,7 @@ def _compute_AOs_value_grad_lap_sphe( gy = _reduce_primitives_to_aos(grad_dup[..., 1], aos_data) gz = _reduce_primitives_to_aos(grad_dup[..., 2], aos_data) - # lap finalize (kept in ao_grad_lap zone — no cast). + # lap finalize (kept in ao_grad_lap zone -- no cast). grad_S_dot_r = jnp.sum(S_l_m_grad_dup * r_R_diffs, axis=-1) lap_dup = ( pref * S_l_m_lap_dup @@ -3584,7 +3584,7 @@ def compute_AOs_value_grad_lap( same call site (kinetic energy, streaming-state initialisation / advance). For value-only, grad-only, or lap-only call sites, prefer the standalone APIs (``compute_AOs`` / ``compute_AOs_grad`` / - ``compute_AOs_laplacian``) — JAX DCE does not reliably eliminate the + ``compute_AOs_laplacian``) -- JAX DCE does not reliably eliminate the unused outputs of this function across its ``@jit`` boundary. Mixed-precision design: shared body (``exp(-Z r^2)``, polynomial / diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 016cfa09..f9462f80 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -616,7 +616,7 @@ def _compute_ecp_local_parts_all_pairs_debug( Returns: float: The sum of local part of the given ECPs with r_up_carts and r_dn_carts. """ - # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). The + # Forward r_up/dn_carts as-is (Principle 3a -- no parameter rebind). The # accumulated scalar V_local is cast to the coulomb zone before return. dtype_np = get_dtype_np("coulomb") @@ -690,7 +690,7 @@ def _compute_ecp_non_local_parts_all_pairs_debug( list[float]: The list of non-local part of the given ECPs with r_up_carts and r_dn_carts. float: sum of the V_nonlocal """ - # Forward r_up/dn_carts/RT as-is (Principle 3a — no parameter rebind). + # Forward r_up/dn_carts/RT as-is (Principle 3a -- no parameter rebind). # Cast RT to coulomb zone at the use site (the grid_points rotation below). dtype_np = get_dtype_np("coulomb") # noqa: F841 @@ -878,7 +878,7 @@ def _compute_ecp_non_local_parts_nearest_neighbors_debug( list[float]: The list of non-local part of the given ECPs with r_up_carts and r_dn_carts. float: sum of the V_nonlocal """ - # Forward r_up/dn_carts/RT as-is (Principle 3a — no parameter rebind). + # Forward r_up/dn_carts/RT as-is (Principle 3a -- no parameter rebind). # Cast RT to coulomb zone at the use site (the grid_points rotation below). dtype_np = get_dtype_np("coulomb") # noqa: F841 @@ -1119,7 +1119,7 @@ def _compute_ecp_coulomb_potential_debug( Returns: float: The sum of non-local part of the given ECPs with r_up_carts and r_dn_carts. """ - # Forward r_up/dn_carts/RT as-is (Principle 3a — no parameter rebind). + # Forward r_up/dn_carts/RT as-is (Principle 3a -- no parameter rebind). ecp_local_parts = _compute_ecp_local_parts_all_pairs_debug( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts @@ -1147,7 +1147,7 @@ def compute_ecp_local_parts_all_pairs( r_up_carts: jax.Array, r_dn_carts: jax.Array, ) -> float: - """Compute local ECP contribution over all nucleus–electron pairs. + """Compute local ECP contribution over all nucleus-electron pairs. Args: coulomb_potential_data (Coulomb_potential_data): ECP parameters and structure data. @@ -1380,7 +1380,7 @@ def _rels_for_electron(r_cart, i_atom_list): def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): # NOTE: see compute_ecp_non_local_parts_nearest_neighbors_fast_update - # below for the rationale — `jax.ops.segment_sum` inside vmap(vmap(...)) + # below for the rationale -- `jax.ops.segment_sum` inside vmap(vmap(...)) # lowers to a 4096*32*1*L-iter while_loop on GPU. For small L # (typically 2-3) a masked dense reduce is dramatically faster. V_l_vmapped = compute_V_l(rel, exponent, coefficient, power) @@ -1520,7 +1520,7 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( ``A_old_inv`` **must** equal ``G(r_up_carts, r_dn_carts)^{-1}`` exactly at the supplied electron positions. Correctness is only guaranteed when the inverse is maintained via **single-electron (rank-1) Sherman-Morrison - updates** starting from a freshly initialized LU inverse — the pattern + updates** starting from a freshly initialized LU inverse -- the pattern used in the MCMC loop. Passing an inverse from a different configuration silently produces incorrect non-local ECP contributions. """ @@ -1630,7 +1630,7 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): # vmap(vmap(...)) the scatter was lowered to a 1-element-per-iter # GPU while_loop with batch dim flattened to 4096*32*1*L = # ~262k iters/call, dominating LRDMC launch storm (see - # work/05nvidia-nsight/analysis.md §13). For small L (typically 2-3 + # work/05nvidia-nsight/analysis.md Section13). For small L (typically 2-3 # for cc-ECP) a masked dense reduce avoids the scatter entirely. V_l_vmapped = compute_V_l(rel, exponent, coefficient, power) mask = ang_mom[:, None] == jnp.arange(global_max_ang_mom_plus_1)[None, :] @@ -1722,7 +1722,7 @@ def compute_ecp_non_local_parts_all_pairs( Nv: int = Nv_default, flag_determinant_only: bool = False, ) -> tuple[list, list, list, float]: - """Compute non-local ECP contribution considering all nucleus–electron pairs. + """Compute non-local ECP contribution considering all nucleus-electron pairs. Args: coulomb_potential_data (Coulomb_potential_data): ECP parameters and structure data. @@ -1795,7 +1795,7 @@ def compute_ecp_non_local_parts_all_pairs( # NOTE: previously two `jax.ops.segment_sum(..., num_segments=num_segments)`. # Replaced with a masked dense reduce (matmul over the kr-pair axis) so the # XLA scatter-pathology that bites V_l (see compute_ecp_non_local_parts_nearest_neighbors_fast_update - # and analysis.md §13/§14) cannot resurface here. n_kr_pairs and num_segments + # and analysis.md Section13/Section14) cannot resurface here. n_kr_pairs and num_segments # are both small (~tens), so the dense reduce is essentially free. nucleus_index_non_local_part_jnp = jnp.asarray(nucleus_index_non_local_part) _aggregator_mask = nucleus_index_non_local_part_jnp[:, None] == jnp.arange(num_segments)[None, :] @@ -2124,7 +2124,7 @@ def compute_ecp_coulomb_potential( float: Sum of local and non-local ECP contributions for the given geometry. """ # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts/RT here (forwarded to downstream - # functions that handle their own use-site casts — Principle 3a). + # functions that handle their own use-site casts -- Principle 3a). ecp_local_parts = compute_ecp_local_parts_all_pairs( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts @@ -2172,7 +2172,7 @@ def compute_ecp_coulomb_potential_fast( This is the fast variant of :func:`compute_ecp_coulomb_potential`. Instead of performing a fresh LU factorisation of the current-configuration geminal matrix, it accepts ``A_old_inv`` directly. This avoids NaN when the current configuration - is near-singular—provided ``A_old_inv`` is the inverse of a nearby, well-conditioned + is near-singular--provided ``A_old_inv`` is the inverse of a nearby, well-conditioned reference configuration (e.g., the previous MCMC step). Args: @@ -2192,13 +2192,13 @@ def compute_ecp_coulomb_potential_fast( ``A_old_inv`` **must** equal ``G(r_up_carts, r_dn_carts)^{-1}`` exactly at the supplied electron positions. Correctness is only guaranteed when the inverse is maintained via **single-electron (rank-1) Sherman-Morrison updates** starting from - a freshly initialized LU inverse — the pattern used in the MCMC loop. If multiple - electrons have moved simultaneously, the Sherman–Morrison rank-1 update used inside + a freshly initialized LU inverse -- the pattern used in the MCMC loop. If multiple + electrons have moved simultaneously, the Sherman-Morrison rank-1 update used inside :func:`compute_ecp_non_local_parts_nearest_neighbors_fast_update` becomes incorrect and the non-local ratios will be silently wrong. """ # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts/RT here (forwarded to downstream - # functions that handle their own use-site casts — Principle 3a). + # functions that handle their own use-site casts -- Principle 3a). ecp_local_parts = compute_ecp_local_parts_all_pairs( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts @@ -2227,7 +2227,7 @@ def _compute_bare_coulomb_potential_debug( r_dn_carts: npt.NDArray[np.float64], ) -> float: """See compute_bare_coulomb_potential_api.""" - # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). The + # Forward r_up/dn_carts as-is (Principle 3a -- no parameter rebind). The # accumulated scalar is cast to the coulomb zone before return (Principle 3b). dtype_np = get_dtype_np("coulomb") @@ -2255,7 +2255,7 @@ def compute_bare_coulomb_potential( r_up_carts: jax.Array, r_dn_carts: jax.Array, ) -> float: - """Compute bare Coulomb interaction (ion–ion, electron–ion, electron–electron). + """Compute bare Coulomb interaction (ion-ion, electron-ion, electron-electron). Args: coulomb_potential_data (Coulomb_potential_data): Structure and charges (effective if ECPs present). @@ -2287,7 +2287,7 @@ def compute_bare_coulomb_potential_el_ion_element_wise( r_up_carts: jax.Array, r_dn_carts: jax.Array, ) -> tuple[jax.Array, jax.Array]: - """Element-wise electron–ion Coulomb interactions. + """Element-wise electron-ion Coulomb interactions. Args: coulomb_potential_data (Coulomb_potential_data): Structure and charges (effective if ECPs present). @@ -2295,7 +2295,7 @@ def compute_bare_coulomb_potential_el_ion_element_wise( r_dn_carts (jax.Array): Down-spin electron Cartesian coordinates with shape ``(N_dn, 3)`` and ``float64`` dtype. Returns: - tuple[jax.Array, jax.Array]: Element-wise ion–electron interactions for up spins and down spins (shape ``(N_up,)`` and ``(N_dn,)``). + tuple[jax.Array, jax.Array]: Element-wise ion-electron interactions for up spins and down spins (shape ``(N_up,)`` and ``(N_dn,)``). """ # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts. el_ion_interaction reconstructs # r_i - r_j in fp64 internally to avoid catastrophic cancellation; a fp32 pre-cast @@ -2335,7 +2335,7 @@ def compute_discretized_bare_coulomb_potential_el_ion_element_wise( r_dn_carts: jax.Array, alat: float, ) -> tuple[jax.Array, jax.Array]: - """Element-wise electron–ion Coulomb interactions with distance floor ``alat``. + """Element-wise electron-ion Coulomb interactions with distance floor ``alat``. Args: coulomb_potential_data (Coulomb_potential_data): Structure and charges (effective if ECPs present). @@ -2344,7 +2344,7 @@ def compute_discretized_bare_coulomb_potential_el_ion_element_wise( alat (float): Minimum allowed distance to avoid divergence. Returns: - tuple[jax.Array, jax.Array]: Element-wise ion–electron interactions for up spins and down spins (shape ``(N_up,)`` and ``(N_dn,)``). + tuple[jax.Array, jax.Array]: Element-wise ion-electron interactions for up spins and down spins (shape ``(N_up,)`` and ``(N_dn,)``). """ # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts. el_ion_interaction reconstructs # r_i - r_j in fp64 internally to avoid catastrophic cancellation. @@ -2384,7 +2384,7 @@ def _compute_bare_coulomb_potential_el_ion_element_wise_debug( r_dn_carts: npt.NDArray[np.float64], ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """See compute_bare_coulomb_potential_api.""" - # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). The + # Forward r_up/dn_carts as-is (Principle 3a -- no parameter rebind). The # accumulators are cast to the coulomb zone before return (Principle 3b). dtype_np = get_dtype_np("coulomb") R_carts = coulomb_potential_data.structure_data._positions_cart_np # fp64 storage accessor @@ -2426,7 +2426,7 @@ def _compute_discretized_bare_coulomb_potential_el_ion_element_wise_debug( alat: float, ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """See compute_bare_coulomb_potential_api.""" - # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). The + # Forward r_up/dn_carts as-is (Principle 3a -- no parameter rebind). The # accumulators are cast to the coulomb zone before return (Principle 3b). dtype_np = get_dtype_np("coulomb") R_carts = coulomb_potential_data.structure_data._positions_cart_np # fp64 storage accessor @@ -2466,14 +2466,14 @@ def compute_bare_coulomb_potential_el_el( r_up_carts: jax.Array, r_dn_carts: jax.Array, ) -> float: - """Electron–electron Coulomb interaction energy. + """Electron-electron Coulomb interaction energy. Args: r_up_carts (jax.Array): Up-spin electron Cartesian coordinates with shape ``(N_up, 3)`` and ``float64`` dtype. r_dn_carts (jax.Array): Down-spin electron Cartesian coordinates with shape ``(N_dn, 3)`` and ``float64`` dtype. Returns: - float: Electron–electron Coulomb energy. + float: Electron-electron Coulomb energy. """ # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts. el_el_interaction reconstructs # r_i - r_j in fp64 internally to avoid catastrophic cancellation. @@ -2522,13 +2522,13 @@ def el_el_interaction(Z_i, Z_j, r_i, r_j): def compute_bare_coulomb_potential_ion_ion( coulomb_potential_data: Coulomb_potential_data, ) -> float: - """Ion–ion Coulomb interaction energy. + """Ion-ion Coulomb interaction energy. Args: coulomb_potential_data (Coulomb_potential_data): Structure and charges (effective if ECPs present). Returns: - float: Ion–ion Coulomb energy. + float: Ion-ion Coulomb energy. """ dtype_jnp = get_dtype_jnp("coulomb") dtype_np = get_dtype_np("coulomb") @@ -2577,7 +2577,7 @@ def compute_bare_coulomb_potential_el_ion( r_up_carts: jax.Array, r_dn_carts: jax.Array, ) -> float: - """Total electron–ion Coulomb interaction energy. + """Total electron-ion Coulomb interaction energy. Args: coulomb_potential_data (Coulomb_potential_data): Structure and charges (effective if ECPs present). @@ -2585,7 +2585,7 @@ def compute_bare_coulomb_potential_el_ion( r_dn_carts (jax.Array): Down-spin electron Cartesian coordinates with shape ``(N_dn, 3)`` and ``float64`` dtype. Returns: - float: Electron–ion Coulomb energy. + float: Electron-ion Coulomb energy. """ # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts (forwarded to el_ion_element_wise which reconstructs in fp64). interactions_el_ion_elements_up, interactions_el_ion_elements_dn = compute_bare_coulomb_potential_el_ion_element_wise( @@ -2605,7 +2605,7 @@ def _compute_coulomb_potential_debug( wavefunction_data: Wavefunction_data = None, ) -> float: """See compute_coulomb_potential_api.""" - # Forward r_up/dn_carts and RT as-is (Principle 3a — no parameter rebind). + # Forward r_up/dn_carts and RT as-is (Principle 3a -- no parameter rebind). # Each downstream debug function casts to its own zone at the use site; # the accumulated scalar is cast to the coulomb zone before return. dtype_np = get_dtype_np("coulomb") @@ -2661,10 +2661,10 @@ def compute_coulomb_potential( wavefunction_data (Wavefunction_data): Wavefunction (geminal + Jastrow) used for ECP ratios; required when ``ecp_flag`` is True. Returns: - float: Sum of bare Coulomb (ion–ion, electron–ion, electron–electron) and ECP (local + non-local) energies. + float: Sum of bare Coulomb (ion-ion, electron-ion, electron-electron) and ECP (local + non-local) energies. """ # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts/RT (forwarded to downstream - # functions that handle their own use-site casts — Principle 3a). + # functions that handle their own use-site casts -- Principle 3a). # all-electron if not coulomb_potential_data.ecp_flag: @@ -2705,7 +2705,7 @@ def compute_coulomb_potential_fast( """Compute total Coulomb energy using a pre-computed geminal inverse for ECP non-local terms. This is the fast variant of :func:`compute_coulomb_potential`. For ECP systems the - non-local Ψ(r')/Ψ(r) ratios are evaluated via + non-local Psi(r')/Psi(r) ratios are evaluated via :func:`compute_ecp_coulomb_potential_fast`, which accepts ``A_old_inv`` directly and therefore avoids the fresh LU factorisation performed by the standard path. This prevents NaN when the current-configuration geminal matrix is nearly singular. @@ -2721,27 +2721,27 @@ def compute_coulomb_potential_fast( wavefunction_data (Wavefunction_data): Wavefunction (geminal + Jastrow) used for ECP ratios; required when ``ecp_flag`` is True. Returns: - float: Sum of bare Coulomb (ion–ion, electron–ion, electron–electron) and ECP (local + non-local) energies. + float: Sum of bare Coulomb (ion-ion, electron-ion, electron-electron) and ECP (local + non-local) energies. Warning: ``A_old_inv`` **must** equal ``G(r_up_carts, r_dn_carts)^{-1}`` exactly at the supplied electron positions. Correctness is only guaranteed when the inverse is maintained via **single-electron (rank-1) Sherman-Morrison updates** starting from - a freshly initialized LU inverse — the pattern used in the MCMC loop. If multiple - electrons have moved simultaneously the underlying Sherman–Morrison rank-1 update is + a freshly initialized LU inverse -- the pattern used in the MCMC loop. If multiple + electrons have moved simultaneously the underlying Sherman-Morrison rank-1 update is incorrect and non-local ratios will be silently wrong. """ # NOTE: Do NOT pre-cast r_up_carts/r_dn_carts/RT (forwarded to downstream - # functions that handle their own use-site casts — Principle 3a). + # functions that handle their own use-site casts -- Principle 3a). - # all-electron — no ECP, no need for A_old_inv + # all-electron -- no ECP, no need for A_old_inv if not coulomb_potential_data.ecp_flag: bare_coulomb_potential = compute_bare_coulomb_potential( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts ) ecp_coulomb_potential = 0 - # pseudo-potential — use pre-computed inverse to avoid fresh LU + # pseudo-potential -- use pre-computed inverse to avoid fresh LU else: bare_coulomb_potential = compute_bare_coulomb_potential( coulomb_potential_data=coulomb_potential_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts diff --git a/jqmc/determinant.py b/jqmc/determinant.py index 7f25bca5..f7431ccd 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -290,7 +290,7 @@ def apply_block_update(self, block: "VariationalParameterBlock") -> "Geminal_dat if block.name == "lambda_matrix": lambda_new = np.array(block.values) - # Symmetrize unconditionally — the method is a no-op for non-symmetric matrices. + # Symmetrize unconditionally -- the method is a no-op for non-symmetric matrices. lambda_new = self.symmetrize_lambda(lambda_new) return Geminal_data( @@ -350,7 +350,7 @@ def symmetrize_lambda(self, mat): When molecular or crystal spatial symmetry is incorporated in the future, **only this method** needs to be extended (e.g. to - average over a symmetry-group orbit) — every call site + average over a symmetry-group orbit) -- every call site automatically follows. Args: @@ -530,7 +530,7 @@ def compute_orb_value_grad_lap_api(self) -> Callable[..., tuple]: """Fused (value, grad, laplacian) api for AOs or MOs. Returns a callable that yields ``(val, gx, gy, gz, lap)`` in a single - dispatch — used by the streaming advance hot path so the heavy block + dispatch -- used by the streaming advance hot path so the heavy block (``exp``, polynomial chain, ``S_l_m``) is shared across val/grad/lap instead of being recomputed three times. """ @@ -848,10 +848,10 @@ def convert_from_AOs_to_MOs( # Augment up-spin MO basis with unpaired directions (Ne_up > Ne_dn) # # The paired-block SVD produces only Ne_dn meaningful left singular - # vectors. The remaining (num_mo − Ne_dn) vectors belong to the + # vectors. The remaining (num_mo - Ne_dn) vectors belong to the # null space and have *arbitrary* orientation, so the unpaired AO # columns may lose rank when projected onto this MO basis (making - # det(Slater) ≈ 0). We fix this by replacing the null-space vectors + # det(Slater) ~= 0). We fix this by replacing the null-space vectors # with orthonormal directions derived from the unpaired AO block, # projected into the orthogonal complement of the paired subspace. # ------------------------------------------------------------------ @@ -868,7 +868,7 @@ def convert_from_AOs_to_MOs( # Replace null-space SVD vectors with unpaired-derived vectors selected_vectors_up = selected_vectors_up.copy() selected_vectors_up[:, ne_dn : ne_dn + n_replace] = Q_unp[:, :n_replace] - # Zero out paired eigenvalues for the replaced positions — these MOs + # Zero out paired eigenvalues for the replaced positions -- these MOs # serve the unpaired block only and must not contribute to the paired part. selected_evals[ne_dn : ne_dn + n_replace] = 0.0 logger.info( @@ -1079,8 +1079,8 @@ def _ln_det_bwd(res, g): implicitly accounts for the pseudoinverse structure (including projection terms when singular values are zeroed). This is why the autodiff kinetic energy remains accurate even for ill-conditioned ``G``, whereas the analytic - Laplacian in :func:`compute_grads_and_laplacian_ln_Det` — which assumes - the simpler ``d(G^{-1}) = -G^{-1} dG G^{-1}`` — becomes approximate. + Laplacian in :func:`compute_grads_and_laplacian_ln_Det` -- which assumes + the simpler ``d(G^{-1}) = -G^{-1} dG G^{-1}`` -- becomes approximate. Args: res: residuals from forward pass @@ -1123,7 +1123,7 @@ def compute_ln_det_geminal_all_elements_fast( Mirrors :func:`compute_ln_det_geminal_all_elements` in the forward direction. The **backward pass** replaces the implicit ``G^{-1}`` computation that JAX would normally perform (via a fresh LU decomposition) with the pre-computed - ``geminal_inv`` — the Sherman-Morrison running inverse. This avoids + ``geminal_inv`` -- the Sherman-Morrison running inverse. This avoids catastrophic NaN for near-singular geminal matrices sampled when ``epsilon_AS > 0``. @@ -1140,7 +1140,7 @@ def compute_ln_det_geminal_all_elements_fast( ``geminal_inv`` **must** equal ``G(r_up_carts, r_dn_carts)^{-1}`` exactly at the supplied electron positions. This is only guaranteed when the inverse is maintained via **single-electron (rank-1) Sherman-Morrison - updates** starting from a freshly initialized LU inverse — the pattern + updates** starting from a freshly initialized LU inverse -- the pattern used in the MCMC loop. Passing an inverse that corresponds to different electron positions silently produces incorrect gradients. """ @@ -1223,7 +1223,7 @@ def _compute_det_geminal_all_elements_debug( def compute_AS_regularization_factor_fast_update( geminal: npt.NDArray[np.float64], geminal_inv: npt.NDArray[np.float64] ) -> jax.Array: - """Compute Attaccalite–Sorella regularization via fast update. + """Compute Attaccalite-Sorella regularization via fast update. Args: geminal: Geminal matrix with shape ``(N_up, N_up)``. @@ -1289,7 +1289,7 @@ def _compute_AS_regularization_factor_debug( @jit def compute_AS_regularization_factor(geminal_data: Geminal_data, r_up_carts: jax.Array, r_dn_carts: jax.Array) -> jax.Array: - """Compute Attaccalite–Sorella regularization from electron coordinates. + """Compute Attaccalite-Sorella regularization from electron coordinates. Args: geminal_data: Geminal parameters and orbital references. @@ -1325,7 +1325,7 @@ def compute_AS_regularization_factor(geminal_data: Geminal_data, r_up_carts: jax ) # compute R_AS - # Guard: S*F can be 0*∞ = NaN when G is near-singular (S→0, F→∞). + # Guard: S*F can be 0*inf = NaN when G is near-singular (S->0, F->inf). # Return 0 in that case to fully down-weight the walker instead of NaN. SF = S * F R_AS = jnp.where(jnp.isfinite(SF) & (SF > 0.0), SF ** (-theta), 0.0) @@ -1346,7 +1346,7 @@ def compute_geminal_all_elements(geminal_data: Geminal_data, r_up_carts: jax.Arr """ # NOTE: do not pre-cast r_*_carts here. r_*_carts is only forwarded to # ``_compute_geminal_all_elements`` (which in turn calls ``compute_orb_api`` - # → ``compute_AOs``); the AO kernels reconstruct ``r - R`` in float64 + # -> ``compute_AOs``); the AO kernels reconstruct ``r - R`` in float64 # internally to avoid catastrophic cancellation, and a wrapper-level # downcast would defeat that guard. Arithmetic in this function uses # ``ao_matrix_*`` / ``lambda_matrix_*`` which are cast at their own use @@ -1563,10 +1563,10 @@ def _compute_ratio_determinant_part_rank1_update( grid generated by the MCMC loop, where exactly one electron is displaced per grid point by construction. """ - # Forward A_old_inv and old/new r_up/dn_carts as-is (Principle 3a — no + # Forward A_old_inv and old/new r_up/dn_carts as-is (Principle 3a -- no # parameter rebind). Module-level forwards (compute_det_geminal_all_elements, # compute_orb_api) handle their own use-site casts. Inline arithmetic below - # casts at the use site (Principle 3b) — see jnp.dot with A_old_inv. + # casts at the use site (Principle 3b) -- see jnp.dot with A_old_inv. dtype_jnp = get_dtype_jnp("det_ratio") num_up = old_r_up_carts.shape[0] num_dn = old_r_dn_carts.shape[0] @@ -1617,7 +1617,7 @@ def _compute_ratio_determinant_part_rank1_update( # row_paired = (orb_up_new^T @ lambda_paired) @ orb_dn_old. # Naively materialising ``orb_up_new^T @ lambda_paired`` produces a # ``(G, n_orb_dn)`` intermediate that is enormous on the ECP / discretized - # kinetic mesh (G ≈ walker * Nv * NN or walker * 6 * n_elec, easily 1-100 M + # kinetic mesh (G ~= walker * Nv * NN or walker * 6 * n_elec, easily 1-100 M # rows for f64 at GH200 scale). Pre-contracting on the small side instead, # M_paired := lambda_paired @ orb_dn_old # (n_orb_up, N_dn) # row_paired = orb_up_new^T @ M_paired # (G, N_dn) @@ -1700,7 +1700,7 @@ def _compute_ratio_determinant_part_split_spin( exclusively for the block-structured non-local ECP grids produced by the MCMC loop. """ - # Forward A_old_inv and old/new r_up/dn coords as-is (Principle 3a — no + # Forward A_old_inv and old/new r_up/dn coords as-is (Principle 3a -- no # parameter rebind). Module-level forwards (compute_orb_api, # _compute_ratio_determinant_part_rank1_update) handle their own use-site # casts. A_old_inv is cast at the use site (Principle 3b) below. @@ -1734,7 +1734,7 @@ def _compute_ratio_determinant_part_split_spin( orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts).astype(dtype_jnp) orb_matrix_dn_old = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, old_r_dn_carts).astype(dtype_jnp) - # ── UP BLOCK: up electron moved, dn unchanged ────────────────────────────── + # --- UP BLOCK: up electron moved, dn unchanged ----------------------------- delta_up = new_r_up_shifted - old_r_up_carts # (G_up, N_up, 3) moved_up_mask = jnp.any(delta_up != 0.0, axis=2) # (G_up, N_up) idx_up = jnp.argmax(moved_up_mask.astype(jnp.int32), axis=1) # (G_up,) @@ -1762,7 +1762,7 @@ def _compute_ratio_determinant_part_split_spin( A_col_for_up = jnp.take(A_old_inv_z, idx_up, axis=1).T # (G_up, N_up) det_ratio_up_block = jnp.sum(new_rows_up * A_col_for_up, axis=1) # (G_up,) - # ── DN BLOCK: dn electron moved, up unchanged ────────────────────────────── + # --- DN BLOCK: dn electron moved, up unchanged ----------------------------- delta_dn = new_r_dn_shifted - old_r_dn_carts # (G_dn, N_dn, 3) moved_dn_mask = jnp.any(delta_dn != 0.0, axis=2) # (G_dn, N_dn) idx_dn = jnp.argmax(moved_dn_mask.astype(jnp.int32), axis=1) # (G_dn,) @@ -1898,7 +1898,7 @@ def compute_grads_and_laplacian_ln_Det( # Single fused dispatch shares the heavy block (exp / poly / S_l_m) across # val/grad/lap and matches the ``_fast`` / ``_grads_lap_body`` paths so all # three produce bit-identical AO outputs. Cast val to det_grad_lap zone - # (fp64) at the use site below — required by Principle 3b since the kernel + # (fp64) at the use site below -- required by Principle 3b since the kernel # zone for downstream einsums is det_grad_lap. ao_matrix_up, ao_matrix_up_grad_x, ao_matrix_up_grad_y, ao_matrix_up_grad_z, ao_matrix_laplacian_up = ( geminal_data.compute_orb_value_grad_lap_api(geminal_data.orb_data_up_spin, r_up_carts) @@ -2091,7 +2091,7 @@ def _grads_lap_bwd(res, g): Step 2 uses ``d(G^{-1}) = -G^{-1} dG G^{-1}``, which is exact only when ``G_inv_stable`` is the true inverse. If ``EPS_rcond_SVD`` is large enough to zero out non-negligible singular values, this identity - becomes approximate — the full pseudoinverse derivative has additional + becomes approximate -- the full pseudoinverse derivative has additional projection terms (see the docstring of :func:`compute_grads_and_laplacian_ln_Det` for details). Keep ``EPS_rcond_SVD`` very small (e.g. ``1e-20``) to avoid this. @@ -2164,7 +2164,7 @@ def compute_grads_and_laplacian_ln_Det_fast( ``geminal_inverse`` **must** equal ``G(r_up_carts, r_dn_carts)^{-1}`` exactly at the supplied electron positions. This is only guaranteed when the inverse is maintained via **single-electron (rank-1) Sherman-Morrison - updates** starting from a freshly initialized LU inverse — the pattern + updates** starting from a freshly initialized LU inverse -- the pattern used in the MCMC loop. Passing an inverse that corresponds to different electron positions silently produces incorrect kinetic energy. """ @@ -2173,7 +2173,7 @@ def compute_grads_and_laplacian_ln_Det_fast( dtype_jnp = get_dtype_jnp("det_grad_lap") # r_*_carts and geminal_inverse are only forwarded; do not pre-cast - # (Principle 3a — no parameter rebind). geminal_inverse is cast to the + # (Principle 3a -- no parameter rebind). geminal_inverse is cast to the # det_grad_lap zone at each einsum use site below (Principle 3b). lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn]) @@ -2252,21 +2252,21 @@ def compute_grads_and_laplacian_ln_Det_fast( # --------------------------------------------------------------------------- # Streaming variant of ``compute_grads_and_laplacian_ln_Det_fast``. # -# Maintains (a) AO/grad/lap tables, (b) λ_p ⨯ ao_dn intermediates, and +# Maintains (a) AO/grad/lap tables, (b) lambda_p x ao_dn intermediates, and # (c) the full geminal grad/lap matrices, all consistent with the current -# (r_up, r_dn). A single-electron move advances them in O(n_ao² + n_ao·N_e -# + N_e²) per call, vs. O(n_ao²·N_e + n_ao·N_e²) for fresh recompute. +# (r_up, r_dn). A single-electron move advances them in O(n_ao^2 + n_ao*N_e +# + N_e^2) per call, vs. O(n_ao^2*N_e + n_ao*N_e^2) for fresh recompute. # -# See ``lrdmc_refactoring.md`` § 1-3 for the field list / advance derivation. +# See ``lrdmc_refactoring.md`` Section 1-3 for the field list / advance derivation. # --------------------------------------------------------------------------- @struct.dataclass class Det_streaming_state: - """Auxiliary tables required to evaluate ``∇ln|Det|`` / ``∇²ln|Det|`` + """Auxiliary tables required to evaluate ``nablaln|Det|`` / ``nabla^2ln|Det|`` incrementally under single-electron moves. - See ``lrdmc_refactoring.md`` § 1-3 for the per-field rationale. + See ``lrdmc_refactoring.md`` Section 1-3 for the per-field rationale. """ ao_up: jax.Array @@ -2332,7 +2332,7 @@ def _init_grads_laplacian_ln_Det_streaming_state( ) -> Det_streaming_state: """Initialize the det streaming state at ``(r_up, r_dn)``. - Cost is dominated by the same ``λ_p ⨯ ao_dn`` and AO einsums as + Cost is dominated by the same ``lambda_p x ao_dn`` and AO einsums as :func:`compute_grads_and_laplacian_ln_Det_fast`; the streaming path additionally retains the Phase-1/2 intermediates needed by the rank-1 advance. @@ -2347,7 +2347,7 @@ def _init_grads_laplacian_ln_Det_streaming_state( lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) # Phase 0: AO/grad/lap evaluation (forward r_*_carts unchanged so the - # underlying kernels reconstruct r-R in fp64 — Principle 3b). Single fused + # underlying kernels reconstruct r-R in fp64 -- Principle 3b). Single fused # dispatch shares the heavy block (exp / poly / S_l_m) across val/grad/lap. ao_up, ao_up_gx, ao_up_gy, ao_up_gz, ao_up_lap = geminal_data.compute_orb_value_grad_lap_api( geminal_data.orb_data_up_spin, r_up_carts @@ -2362,12 +2362,12 @@ def _init_grads_laplacian_ln_Det_streaming_state( ao_up_lap = jnp.asarray(ao_up_lap, dtype=dtype_jnp) ao_dn_lap = jnp.asarray(ao_dn_lap, dtype=dtype_jnp) - # Phase 1: λ_p ⨯ ao_dn family (depends on dn only). + # Phase 1: lambda_p x ao_dn family (depends on dn only). paired_dn = lambda_matrix_paired @ ao_dn paired_dn_grads = jnp.einsum("ab,gbn->gan", lambda_matrix_paired, ao_dn_grads) paired_dn_lap = lambda_matrix_paired @ ao_dn_lap - # Phase 2: full geminal grad/lap matrices (paired ‖ unpaired hstack'd). + # Phase 2: full geminal grad/lap matrices (paired || unpaired hstack'd). geminal_grad_up_paired = jnp.einsum("gia,aj->gij", jnp.swapaxes(ao_up_grads, 1, 2), paired_dn) geminal_grad_up_unpaired = jnp.einsum("gia,ak->gik", jnp.swapaxes(ao_up_grads, 1, 2), lambda_matrix_unpaired) geminal_grad_up = jnp.concatenate([geminal_grad_up_paired, geminal_grad_up_unpaired], axis=2) @@ -2440,7 +2440,7 @@ def _advance_grads_laplacian_ln_Det_streaming_state( inverse of ``G(r_up_new, r_dn_new)`` provided by the caller (typically ``_body_step_core`` in ``jqmc_gfmc.py``). - Cost: ``O(n_ao² + n_ao · N_e + N_e²)`` per call. + Cost: ``O(n_ao^2 + n_ao * N_e + N_e^2)`` per call. """ dtype_jnp = get_dtype_jnp("det_grad_lap") @@ -2455,7 +2455,7 @@ def _advance_grads_laplacian_ln_Det_streaming_state( def _branch_up(_): # --- Phase 0: single-point AO eval at r_up_new[k] ----------------- # Single fused dispatch shares the heavy block (exp / poly / S_l_m) - # across val/grad/lap — replaces three separate compute_orb_* calls. + # across val/grad/lap -- replaces three separate compute_orb_* calls. r_new = jnp.expand_dims(r_up_carts_new[moved_index], axis=0) # (1, 3) ao_v, gx, gy, gz, ao_lap = geminal_data.compute_orb_value_grad_lap_api(geminal_data.orb_data_up_spin, r_new) ao_col = jnp.asarray(ao_v[:, 0], dtype=dtype_jnp) # (n_ao_up,) @@ -2466,9 +2466,9 @@ def _branch_up(_): new_ao_up_grads = state.ao_up_grads.at[:, :, moved_index].set(grad_col) new_ao_up_lap = state.ao_up_lap.at[:, moved_index].set(lap_col) - # --- Phase 2: row k of geminal_* (paired ‖ unpaired) -------------- - # row of geminal_grad_up: einsum("ga,aj->gj", grad_col, paired_dn) ‖ - # einsum("ga,ak->gk", grad_col, λ_u) + # --- Phase 2: row k of geminal_* (paired || unpaired) -------------- + # row of geminal_grad_up: einsum("ga,aj->gj", grad_col, paired_dn) || + # einsum("ga,ak->gk", grad_col, lambda_u) row_grad_up_paired = jnp.einsum("ga,aj->gj", grad_col, state.paired_dn) row_grad_up_unpaired = jnp.einsum("ga,ak->gk", grad_col, lambda_matrix_unpaired) row_grad_up = jnp.concatenate([row_grad_up_paired, row_grad_up_unpaired], axis=1) @@ -2481,13 +2481,13 @@ def _branch_up(_): row_grad_dn = jnp.concatenate([row_grad_dn_paired, row_grad_dn_unpaired], axis=1) new_geminal_grad_dn = state.geminal_grad_dn.at[:, moved_index, :].set(row_grad_dn) - # row of geminal_lap_up: lap_col @ paired_dn ‖ lap_col @ λ_u + # row of geminal_lap_up: lap_col @ paired_dn || lap_col @ lambda_u row_lap_up_paired = lap_col @ state.paired_dn row_lap_up_unpaired = lap_col @ lambda_matrix_unpaired row_lap_up = jnp.concatenate([row_lap_up_paired, row_lap_up_unpaired], axis=0) new_geminal_lap_up = state.geminal_lap_up.at[moved_index, :].set(row_lap_up) - # row of geminal_lap_dn: ao_col @ paired_dn_lap ‖ zeros + # row of geminal_lap_dn: ao_col @ paired_dn_lap || zeros row_lap_dn_paired = ao_col @ state.paired_dn_lap row_lap_dn_unpaired = jnp.zeros((num_up - num_dn,), dtype=dtype_jnp) row_lap_dn = jnp.concatenate([row_lap_dn_paired, row_lap_dn_unpaired], axis=0) @@ -2625,7 +2625,7 @@ def _compute_grads_and_laplacian_ln_Det_auto( Uses autodiff on ln|det(G)| to compute gradients w.r.t. electron positions and per-electron Laplacians. """ - # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). Cast + # Forward r_up/dn_carts as-is (Principle 3a -- no parameter rebind). Cast # to the det_grad_lap zone at the use site before passing as the # differentiation operand to grad/jacfwd (Principle 3b). dtype_jnp = get_dtype_jnp("det_grad_lap") @@ -2829,10 +2829,10 @@ def _compute_grads_and_laplacian_ln_Det_debug( ############################################################# # Laplacian part (4th-order central finite differences) - # f''(x) ≈ (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h²) + # f''(x) ~= (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h^2) ############################################################# - diff_h2 = 1.0e-3 # larger h viable with 4th-order stencil (O(h⁴) truncation) + diff_h2 = 1.0e-3 # larger h viable with 4th-order stencil (O(h^4) truncation) laplacian_ln_D_up = np.zeros(len(r_up_carts)) laplacian_ln_D_dn = np.zeros(len(r_dn_carts)) diff --git a/jqmc/hamiltonians.py b/jqmc/hamiltonians.py index 7104225b..28759cb6 100644 --- a/jqmc/hamiltonians.py +++ b/jqmc/hamiltonians.py @@ -200,7 +200,7 @@ def compute_local_energy( float: The value of local energy (e_L) with the given wavefunction (float) """ dtype_jnp = get_dtype_jnp("local_energy") - # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). Each + # Forward r_up/dn_carts as-is (Principle 3a -- no parameter rebind). Each # downstream consumer (compute_kinetic_energy, compute_coulomb_potential) # casts to its own zone at the use site. @@ -233,10 +233,10 @@ def compute_local_energy_fast( Identical to :func:`compute_local_energy` but avoids re-computing the LU decomposition of the geminal matrix by reusing ``geminal_inverse`` - supplied by the caller (e.g. the Sherman–Morrison inverse maintained + supplied by the caller (e.g. the Sherman-Morrison inverse maintained inside the MCMC loop). When the geminal matrix is near-singular the fresh LU decomposition inside :func:`compute_local_energy` produces - NaN, whereas the Sherman–Morrison inverse has already been regularized + NaN, whereas the Sherman-Morrison inverse has already been regularized by the AS acceptance/rejection, making this variant numerically safer. Args: @@ -250,7 +250,7 @@ def compute_local_energy_fast( Rotation matrix (R.T) used for the non-local ECP part. geminal_inverse (jnpt.ArrayLike): Precomputed inverse of the geminal matrix ``G(r_up_carts, r_dn_carts)`` - with shape ``(N_up, N_up)``. Typically the Sherman–Morrison running + with shape ``(N_up, N_up)``. Typically the Sherman-Morrison running inverse from the MCMC loop. Returns: @@ -261,12 +261,12 @@ def compute_local_energy_fast( exactly at the supplied electron positions. Correctness is only guaranteed when the inverse is maintained via **single-electron (rank-1) Sherman-Morrison updates** starting from a freshly - initialized LU inverse — the pattern used in the MCMC loop. + initialized LU inverse -- the pattern used in the MCMC loop. Passing an inverse from a different configuration silently produces incorrect kinetic energy. """ dtype_jnp = get_dtype_jnp("local_energy") - # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). Each + # Forward r_up/dn_carts as-is (Principle 3a -- no parameter rebind). Each # downstream consumer casts to its own zone at the use site. T_up_elements, T_dn_elements = compute_kinetic_energy_all_elements_fast_update( @@ -315,7 +315,7 @@ def _compute_local_energy_auto( float: The value of local energy (e_L) with the given wavefunction (float) """ dtype_jnp = get_dtype_jnp("local_energy") - # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). Each + # Forward r_up/dn_carts as-is (Principle 3a -- no parameter rebind). Each # downstream consumer casts to its own zone at the use site. T = _compute_kinetic_energy_auto( @@ -514,7 +514,7 @@ def _load_dataclass_from_hdf5(cls: Type[T], group: h5py.Group) -> T: # Convert np.ndarray or list/tuple to jax.Array for fields typed as jax.Array. # Note: fields typed `npt.NDArray[np.float64]` (string-form annotation) must - # NOT trigger this branch — they are stored as numpy arrays. Exclude both + # NOT trigger this branch -- they are stored as numpy arrays. Exclude both # "ndarray" (resolved form) and "NDArray" (npt alias form). if ( isinstance(val, (np.ndarray, list, tuple)) diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index f6749283..1ee20de6 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -202,7 +202,7 @@ class NNJastrow(nn.Module): r"""PauliNet-inspired NN that outputs a three-body Jastrow correction. The network implements the iteration rules described in the PauliNet - manuscript (Eq. 1–2). Electron embeddings :math:`\mathbf{x}_i^{(n)}` are + manuscript (Eq. 1-2). Electron embeddings :math:`\mathbf{x}_i^{(n)}` are iteratively refined by three message channels: * ``(+ )``: same-spin electrons, enforcing antisymmetry indirectly by keeping @@ -522,7 +522,7 @@ def __call__( The network is permutation equivariant within each spin channel and rotation invariant by construction of the PhysNet radial features. """ - # Forward r_up/r_dn/R_n as-is (Principle 3a — no parameter rebind). + # Forward r_up/r_dn/R_n as-is (Principle 3a -- no parameter rebind). # `_pairwise_distances` reconstructs the differences in caller-supplied # precision and downcasts to the jastrow_eval zone at the use site. Z_n = jnp.asarray(Z_n) @@ -561,16 +561,16 @@ def __call__( class Jastrow_one_body_data: r"""One-body Jastrow parameters and structure metadata. - The one-body term models electron–nucleus correlations. Two functional + The one-body term models electron-nucleus correlations. Two functional forms are available, selected by ``jastrow_1b_type``: - * ``'exp'`` (default) — exponential form: + * ``'exp'`` (default) -- exponential form: .. math:: f(r_{eN}) = -A \, \frac{1}{2a} \bigl(1 - e^{-a\,c\,r_{eN}}\bigr) - * ``'pade'`` — Padé form: + * ``'pade'`` -- Pade form: .. math:: @@ -584,7 +584,7 @@ class Jastrow_one_body_data: Args: jastrow_1b_param (float): Parameter *a* controlling the one-body decay. - jastrow_1b_type (str): Functional form — ``'exp'`` or ``'pade'``. + jastrow_1b_type (str): Functional form -- ``'exp'`` or ``'pade'``. Stored as a compile-time constant (``pytree_node=False``). structure_data (Structure_data): Nuclear positions and charges. core_electrons (tuple[float]): Removed core electrons per nucleus (for ECPs). @@ -1083,7 +1083,7 @@ def _advance_grads_laplacian_Jastrow_one_body_streaming_state( num_dn = state.grad_J1_dn.shape[0] def _branch_up(_): - # Reuse the full-batch kernel on a length-1 slice — gives one row that + # Reuse the full-batch kernel on a length-1 slice -- gives one row that # we slot back into the cached state. r_slice = jnp.expand_dims(r_up_carts_new[moved_index], axis=0) # (1, 3) g_row, _, l_row, _ = compute_grads_and_laplacian_Jastrow_one_body(jastrow_one_body_data, r_slice, r_slice[:0]) @@ -1094,7 +1094,7 @@ def _branch_up(_): def _branch_dn(_): r_slice = jnp.expand_dims(r_dn_carts_new[moved_index], axis=0) # Pass empty up so only the dn branch contributes (J1 is per-spin - # independent — feeding empty up has zero effect on dn output). + # independent -- feeding empty up has zero effect on dn output). _, g_row, _, l_row = compute_grads_and_laplacian_Jastrow_one_body(jastrow_one_body_data, r_slice[:0], r_slice) new_grad = state.grad_J1_dn.at[moved_index].set(g_row[0]) new_lap = state.lap_J1_dn.at[moved_index].set(l_row[0]) @@ -1111,16 +1111,16 @@ def _branch_dn(_): class Jastrow_two_body_data: r"""Two-body Jastrow parameter container. - The two-body term models electron–electron correlations. Two functional + The two-body term models electron-electron correlations. Two functional forms are available, selected by ``jastrow_2b_type``: - * ``'pade'`` (default) — Padé form: + * ``'pade'`` (default) -- Pade form: .. math:: f(r_{ee}) = \frac{r_{ee}}{2\,(1 + a\,r_{ee})} - * ``'exp'`` — exponential form: + * ``'exp'`` -- exponential form: .. math:: @@ -1133,7 +1133,7 @@ class Jastrow_two_body_data: Args: jastrow_2b_param (float): Parameter *a* for the two-body Jastrow part. - jastrow_2b_type (str): Functional form — ``'pade'`` or ``'exp'``. + jastrow_2b_type (str): Functional form -- ``'pade'`` or ``'exp'``. Stored as a compile-time constant (``pytree_node=False``). """ @@ -2012,7 +2012,7 @@ def apply_block_update(self, block: "VariationalParameterBlock") -> "Jastrow_dat elif block.name == "j3_matrix" and j3 is not None: j3_new = np.array(block.values, dtype=dtype_np) - # Symmetrize unconditionally — the method is a no-op for non-symmetric matrices. + # Symmetrize unconditionally -- the method is a no-op for non-symmetric matrices. j3_new = self.symmetrize_j3(j3_new) j3 = Jastrow_three_body_data(orb_data=j3.orb_data, j_matrix=j3_new) @@ -2059,7 +2059,7 @@ def symmetrize_j3(self, mat): When molecular or crystal spatial symmetry is incorporated in the future, **only this method** needs to be extended (e.g. to - average over a symmetry-group orbit) — every call site + average over a symmetry-group orbit) -- every call site automatically follows. Args: @@ -2250,7 +2250,7 @@ def _compute_ratio_Jastrow_part_rank1_update( j3_state: Optional cached J3 auxiliaries consistent with ``(old_r_up_carts, old_r_dn_carts)``. When provided, the J3 block reuses ``aos_*``, ``j3_mat @ aos_*``, ``j3_mat.T @ aos_*`` from the - state instead of recomputing them — saves per-call ``O(n_ao^2 * N_e)`` + state instead of recomputing them -- saves per-call ``O(n_ao^2 * N_e)`` in matmul work. Pass ``None`` (default) to recompute from scratch (the original 1-shot path used outside the projection loop). @@ -2267,11 +2267,11 @@ def _compute_ratio_Jastrow_part_rank1_update( grid generated by the MCMC loop, where exactly one electron is displaced per grid point by construction. """ - # Forward old/new r_up/dn_carts as-is (Principle 3a — no parameter rebind). + # Forward old/new r_up/dn_carts as-is (Principle 3a -- no parameter rebind). # Module-level forwards (compute_Jastrow_part, compute_Jastrow_one_body, # compute_orb_api, NN_Jastrow.apply) handle their own use-site casts. # Inline arithmetic in the local J1/J2/J3 closures below casts at the diff - # site (Principle 3b) — for r-r differences the operand is reconstructed in + # site (Principle 3b) -- for r-r differences the operand is reconstructed in # caller-supplied precision (fp64 from MCMC walker state) before downcast. dtype_jnp = get_dtype_jnp("jastrow_ratio") @@ -2566,7 +2566,7 @@ def _batch_pairwise_sum(points_a, points_b, param): J_ratio *= jnp.ravel(J2_ratio) - # J3 part (batched AO evaluation — avoids per-config compute_orb_api inside vmap) + # J3 part (batched AO evaluation -- avoids per-config compute_orb_api inside vmap) if jastrow_data.jastrow_three_body_data is not None: j3d = jastrow_data.jastrow_three_body_data j3_mat = j3d._j_matrix_jnp[:, :-1] # (n_ao, n_ao) shared for up-up / dn-dn / up-dn @@ -2575,7 +2575,7 @@ def _batch_pairwise_sum(points_a, points_b, param): # Old AOs evaluated once. # When ``j3_state`` is supplied, the cached AOs/W/U/cross_vec from the # streaming state are consistent with ``(old_r_up_carts, old_r_dn_carts)`` - # by contract — we just dtype-cast into the jastrow_ratio zone and skip + # by contract -- we just dtype-cast into the jastrow_ratio zone and skip # the recomputation. Python-static dispatch (j3_state is None vs not). if j3_state is None: aos_up_old = jnp.array(j3d.compute_orb_api(j3d.orb_data, old_r_up_carts), dtype=dtype_jnp) # (n_ao, N_up) @@ -2608,10 +2608,10 @@ def _batch_pairwise_sum(points_a, points_b, param): # Precompute constant products (independent of config). With a # streaming state, all four matmuls are read directly from the cache - # — that's the main per-step ``O(n_ao^2 * N_e)`` saving. Note that + # -- that's the main per-step ``O(n_ao^2 * N_e)`` saving. Note that # ``j3_state.j3_mat_T_aos_*`` stores ``j3_mat.T @ aos_*`` of shape # ``(n_ao, N_*)``, while we want ``U_* = aos_*.T @ j3_mat`` of shape - # ``(N_*, n_ao)`` — these are transposes of each other. + # ``(N_*, n_ao)`` -- these are transposes of each other. if j3_state is None: W_up = jnp.dot(j3_mat, aos_up_old) # (n_ao, N_up) = j3_mat @ A_up U_up = jnp.dot(aos_up_old.T, j3_mat) # (N_up, n_ao) = A_up.T @ j3_mat @@ -2714,7 +2714,7 @@ def _compute_ratio_Jastrow_part_split_spin( When called from the projection streaming path, the caller may pass ``j3_state`` to skip recomputing ``aos_*_old`` and the ``W``/``U``/cross_vec - products — see ``_compute_ratio_Jastrow_part_rank1_update`` for the exact + products -- see ``_compute_ratio_Jastrow_part_rank1_update`` for the exact correspondence. Args: @@ -2739,7 +2739,7 @@ def _compute_ratio_Jastrow_part_split_spin( exclusively for the block-structured non-local ECP grids produced by the MCMC loop. """ - # Forward old/new r_up/dn_carts as-is (Principle 3a — no parameter rebind). + # Forward old/new r_up/dn_carts as-is (Principle 3a -- no parameter rebind). # Module-level forwards (compute_Jastrow_one_body, compute_orb_api, # _compute_ratio_Jastrow_part_rank1_update, NN_Jastrow.apply) handle their # own use-site casts. Inline diffs in the local J2 _safe_norm closure cast @@ -2790,7 +2790,7 @@ def _compute_ratio_Jastrow_part_split_spin( J_up = jnp.ones(g_up, dtype=dtype_jnp) J_dn = jnp.ones(g_dn, dtype=dtype_jnp) - # ── J1 part ────────────────────────────────────────────────────────────── + # -- J1 part -------------------------------------------------------------- if jastrow_data.jastrow_one_body_data is not None: j1_data = jastrow_data.jastrow_one_body_data @@ -2812,7 +2812,7 @@ def compute_J1_dn_one(r_dn_new: jax.Array, r_dn_old: jax.Array) -> jax.Array: J1_dn_block = vmap(compute_J1_dn_one)(r_dn_moved, r_dn_old_moved) # (G_dn,) J_dn = J_dn * jnp.ravel(J1_dn_block) - # ── J2 part ────────────────────────────────────────────────────────────── + # -- J2 part -------------------------------------------------------------- if jastrow_data.jastrow_two_body_data is not None: j2_param = jastrow_data.jastrow_two_body_data.jastrow_2b_param _j2_type_split = jastrow_data.jastrow_two_body_data.jastrow_2b_type @@ -2870,7 +2870,7 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: J2_dn_up_old = J2_sum_dn_up[idx_dn_block] # (G_dn,) J_dn = J_dn * jnp.exp(J2_dn_up_new - J2_dn_up_old + J2_dn_dn_new - J2_dn_dn_old) - # ── J3 part ────────────────────────────────────────────────────────────── + # -- J3 part -------------------------------------------------------------- if jastrow_data.jastrow_three_body_data is not None: j3d = jastrow_data.jastrow_three_body_data j3_mat = j3d._j_matrix_jnp[:, :-1] # (n_ao, n_ao) @@ -2899,7 +2899,7 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: dn_cross_vec = jnp.sum(W_dn, axis=1) up_cross_vec = jnp.sum(j3_state.j3_mat_T_aos_up.astype(dtype_jnp), axis=1) - # ── UP BLOCK ───────────────────────────────────────────────────────── + # -- UP BLOCK --------------------------------------------------------- # New AOs at the moved up-electron positions; old AOs by column-slice. aos_up_new_moved = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_up_moved), dtype=dtype_jnp) # (n_ao, G_up) aos_up_old_moved = aos_up_old[:, idx_up_block] # (n_ao, G_up) @@ -2907,7 +2907,7 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: term1_up = j1_vec @ aos_p_up # (G_up,) # tensordot avoids the explicit transpose of ``aos_p_up`` (1.8 GB on - # GH200 ECP-nonlocal benchmark) — see ``_compute_ratio_Jastrow_part_rank1_update`` + # GH200 ECP-nonlocal benchmark) -- see ``_compute_ratio_Jastrow_part_rank1_update`` # for the same rewrite rationale. V_up_block = jnp.tensordot(aos_p_up, W_up, axes=((0,), (0,))) # (G_up, N_up) P_up_block = jnp.dot(U_up, aos_p_up) # (N_up, G_up) @@ -2918,7 +2918,7 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: term4_up = dn_cross_vec @ aos_p_up # (G_up,) J_up = J_up * jnp.exp(term1_up + term2_up + term3_up + term4_up) - # ── DN BLOCK ───────────────────────────────────────────────────────── + # -- DN BLOCK --------------------------------------------------------- # New AOs at the moved dn-electron positions; old AOs by column-slice. aos_dn_new_moved = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_dn_moved), dtype=dtype_jnp) # (n_ao, G_dn) aos_dn_old_moved = aos_dn_old[:, idx_dn_block] # (n_ao, G_dn) @@ -2935,7 +2935,7 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: term4_dn = up_cross_vec @ aos_p_dn # (G_dn,) J_dn = J_dn * jnp.exp(term1_dn + term2_dn + term3_dn + term4_dn) - # ── JNN part ───────────────────────────────────────────────────────────── + # -- JNN part ------------------------------------------------------------- if jastrow_data.jastrow_nn_data is not None: nn = jastrow_data.jastrow_nn_data if nn.structure_data is None: @@ -3072,7 +3072,7 @@ def _compute_Jastrow_nn_only(r_up, r_dn): grad_JNN_dn = grad(_compute_Jastrow_nn_only, argnums=1)(r_up_carts_jnp, r_dn_carts_jnp) # Compute per-electron Laplacian via forward-over-reverse (diagonal Hessian only). - # This produces an O(n) computation graph instead of O(n²) from hessian(), + # This produces an O(n) computation graph instead of O(n^2) from hessian(), # which significantly reduces the XLA kernel size when grad(compute_local_energy) # differentiates through the kinetic energy (i.e. under 3rd-order AD). def _lap_jvp(f_r, r): @@ -3504,9 +3504,9 @@ def pair_terms(diff): # path emitted four ``scatter-add`` ops per spin (grad_i/grad_j/lap_i/lap_j). # Because ``idx_i`` repeats values, XLA must respect sequential semantics and # lowers each scatter to a ``while_loop`` of ``trip_count = N*(N-1)/2`` - # (≈ 4 small kernel launches per iteration), which dominates host launch + # (~= 4 small kernel launches per iteration), which dominates host launch # overhead in ``compute_kinetic_energy_all_elements_fast_update`` (8 such - # while-loops total → ~16k launches per call for N=32). Replacing with a + # while-loops total -> ~16k launches per call for N=32). Replacing with a # dense (N,N) reduction collapses the work into a handful of fused # elementwise + reduction kernels with no scatter. # @@ -3515,7 +3515,7 @@ def pair_terms(diff): # ``j`` reproduces the (i,j) and (j,i) contributions of the original # triu-loop. The diagonal i==j is masked out (``r=0`` is clamped to ``eps`` # by ``pair_terms`` and would otherwise add a spurious ~1/eps^2 term to the - # Laplacian). The grad diagonal is mathematically zero (grad_pair ∝ diff) + # Laplacian). The grad diagonal is mathematically zero (grad_pair prop diff) # but is masked too to avoid 0*finite issues if ``eps`` is very small. # NaN-safety note: the diagonal i==j has ``diff = 0``. Even though we mask # the diagonal out of the sum, ``pair_terms`` evaluates ``r = sqrt(sum diff^2)`` @@ -3561,10 +3561,10 @@ def pair_terms(diff): # --------------------------------------------------------------------------- # J2 streaming state (PR3). # -# When a single electron k of spin σ moves, only pair contributions involving +# When a single electron k of spin sigma moves, only pair contributions involving # k change. The state caches per-electron grad/lap and the previous (r_up, # r_dn) so the advance can compute the per-pair delta for that electron. -# Cost: O(N_e) per advance, vs O(N_e²) fresh. +# Cost: O(N_e) per advance, vs O(N_e^2) fresh. # --------------------------------------------------------------------------- @@ -3572,7 +3572,7 @@ def pair_terms(diff): class Jastrow_two_body_streaming_state: """Cached J2 grad/lap and electron coordinates consistent with the state.""" - r_up_carts: jax.Array # (N_up, 3) — config used for the cached J2 quantities + r_up_carts: jax.Array # (N_up, 3) -- config used for the cached J2 quantities r_dn_carts: jax.Array # (N_dn, 3) grad_J2_up: jax.Array # (N_up, 3) grad_J2_dn: jax.Array # (N_dn, 3) @@ -3590,7 +3590,7 @@ def _j2_pair_terms(j2b_type: str, a: jax.Array, eps: jax.Array, diff: jax.Array) Callers may construct ``diff`` in caller-supplied precision (e.g. fp64 walker coords for ``r - r_new``); cast at the arithmetic use site here so pair-term outputs always live in this function's own zone - (``jastrow_grad_lap``) regardless of input dtype (Principle 3b — and + (``jastrow_grad_lap``) regardless of input dtype (Principle 3b -- and required for fori_loop carry-shape stability under mixed precision, where state.r_up_carts is stored in fp64 but state.grad_J2_up lives in the grad/lap zone). The cast target is fetched via @@ -3626,7 +3626,7 @@ def _init_grads_laplacian_Jastrow_two_body_streaming_state( """Build a J2 state at ``(r_up, r_dn)`` via the existing fresh kernel. Stores ``r_up_carts`` / ``r_dn_carts`` in caller-supplied precision - (Principle 3a — no rebind). Under mixed precision the carry-shape + (Principle 3a -- no rebind). Under mixed precision the carry-shape must match what ``advance`` writes back via ``state.r_*.at[moved_index].set(r_up_carts_new[moved_index])``; ``r_up_carts_new`` arrives in fp64 (walker state), so the cached @@ -3671,7 +3671,7 @@ def _branch_up(_): r_old = state.r_up_carts[moved_index] r_new = r_up_carts_new[moved_index] - # --- Same-spin (up-up) pairs (k, i) for i ≠ k -------------------- + # --- Same-spin (up-up) pairs (k, i) for i != k -------------------- # Old & new diffs both place 0 at i=k (state.r_up_carts[k]=r_old vs r_old, # r_up_carts_new[k]=r_new vs r_new), so masking is implicit at i=k for new # but not for old. Mask out the i=k row explicitly to avoid contaminating k @@ -3686,7 +3686,7 @@ def _branch_up(_): delta_grad_uu = delta_grad_uu * mask_uu[:, None] delta_lap_uu = delta_lap_uu * mask_uu - # i ≠ k: grad_up[i] -= delta_grad_uu[i], lap_up[i] += delta_lap_uu[i] + # i != k: grad_up[i] -= delta_grad_uu[i], lap_up[i] += delta_lap_uu[i] new_grad_up = state.grad_J2_up - delta_grad_uu new_lap_up = state.lap_J2_up + delta_lap_uu # k: grad_up[k] += sum delta_grad_uu, lap_up[k] += sum delta_lap_uu @@ -3737,7 +3737,7 @@ def _branch_dn(_): # --- Cross-spin (up-dn): grad_up[i] receives +grad_pair(r_up[i] - r_dn[k]) # so for dn-k moving, the deltas flip signs vs the up branch: - # diff = r_up[i] - r_dn_* → diff_new for r_dn[k]=r_new is r_up[i] - r_new + # diff = r_up[i] - r_dn_* -> diff_new for r_dn[k]=r_new is r_up[i] - r_new diff_old_du = state.r_up_carts - r_old[None, :] # (N_up, 3) diff_new_du = state.r_up_carts - r_new[None, :] # (N_up, 3) grad_old_du, lap_old_du = _j2_pair_terms(j2b_type, a, eps, diff_old_du) @@ -3748,7 +3748,7 @@ def _branch_dn(_): # grad_up[i] += delta_grad_du[i] (sign +) new_grad_up = state.grad_J2_up + delta_grad_du new_lap_up = state.lap_J2_up + delta_lap_du - # grad_dn[k] -= sum_i delta_grad_du[i] (sign − accumulated at k) + # grad_dn[k] -= sum_i delta_grad_du[i] (sign - accumulated at k) new_grad_dn = new_grad_dn.at[moved_index].add(-jnp.sum(delta_grad_du, axis=0)) new_lap_dn = new_lap_dn.at[moved_index].add(jnp.sum(delta_lap_du, axis=0)) @@ -4045,7 +4045,7 @@ def _compute_grads_and_laplacian_Jastrow_three_body_auto( Returns: the gradients(x,y,z) of J(threebody) and the sum of laplacians of J(threebody) at (r_up_carts, r_dn_carts). """ - # Forward r_up/dn_carts as-is (Principle 3a — no parameter rebind). Cast to + # Forward r_up/dn_carts as-is (Principle 3a -- no parameter rebind). Cast to # the jastrow_grad_lap zone at the use site (Principle 3b) before passing as # the differentiation operand to grad/hessian. dtype_jnp = get_dtype_jnp("jastrow_grad_lap") @@ -4259,7 +4259,7 @@ def _init_grads_laplacian_Jastrow_three_body_streaming_state( compute_orb, compute_orb_grad, compute_orb_lapl, compute_orb_vgl = _three_body_orb_apis(jastrow_three_body_data) # AO/MO tables (forward r_*_carts unchanged so the underlying kernels can - # reconstruct r-R in float64 — Principle 3b). Single fused dispatch shares + # reconstruct r-R in float64 -- Principle 3b). Single fused dispatch shares # the heavy block (exp / poly / S_l_m) across val/grad/lap. aos_up, grad_up_x, grad_up_y, grad_up_z, lap_aos_up = compute_orb_vgl(orb_data, r_up_carts) aos_dn, grad_dn_x, grad_dn_y, grad_dn_z, lap_aos_dn = compute_orb_vgl(orb_data, r_dn_carts) @@ -4340,7 +4340,7 @@ def _advance_grads_laplacian_Jastrow_three_body_streaming_state( represented by ``state`` in *exactly one* electron position, identified by ``(moved_spin_is_up, moved_index)``. If neither spin actually moved (e.g. a no-op step), the state should still be passed through unchanged by the - caller — this routine assumes a real one-electron displacement. + caller -- this routine assumes a real one-electron displacement. Cost: ``O(n_ao^2 + n_ao * N_e)`` per call, dominated by two ``n_ao``-sized matvecs ``j3_mat @ delta_aos`` and one full einsum over ``g``. @@ -4357,7 +4357,7 @@ def _advance_grads_laplacian_Jastrow_three_body_streaming_state( def _branch_up(_): # Single-point AO eval at the moved electron's new position. - # NB: forward r_up_carts_new unchanged (Principle 3b — fp64 r-R + # NB: forward r_up_carts_new unchanged (Principle 3b -- fp64 r-R # reconstruction inside the kernels). Single fused dispatch shares # the heavy block (exp / poly / S_l_m) across val/grad/lap. r_new = jnp.expand_dims(r_up_carts_new[moved_index], axis=0) # (1, 3) @@ -4386,7 +4386,7 @@ def _branch_up(_): new_g_up = state.g_up + d_J[:, None] * mask_lt[None, :] + d_JT[:, None] * mask_gt[None, :] # g_dn update: term C is (j3_mat.T @ aos_up) @ ones_up, so the change - # is sum_k Δ(j3_mat.T @ aos_up)[:, k] = d_JT (single column changed). + # is sum_k Delta(j3_mat.T @ aos_up)[:, k] = d_JT (single column changed). # Same vector added to every dn column. new_g_dn = state.g_dn + d_JT[:, None] @@ -4396,7 +4396,7 @@ def _branch_up(_): new_lap_aos_up = state.lap_aos_up.at[:, moved_index].set(lap_aos_new_col) # Recompute per-electron grad_J3_*, lap_J3_* via einsum on updated - # tables. Cost: O(n_ao * N_e * 3) — within target asymptotics. + # tables. Cost: O(n_ao * N_e * 3) -- within target asymptotics. grad_J3_up = jnp.einsum("on,onj->nj", new_g_up, new_grad_aos_up) grad_J3_dn = jnp.einsum("on,onj->nj", new_g_dn, state.grad_aos_dn) lap_J3_up = jnp.einsum("on,on->n", new_g_up, new_lap_aos_up) @@ -4462,7 +4462,7 @@ def _branch_dn(_): lap_J3_dn=lap_J3_dn, ) - # Edge case: zero-electron spin sector — no advance possible, just no-op. + # Edge case: zero-electron spin sector -- no advance possible, just no-op. if num_up == 0: return _branch_dn(None) if num_dn == 0: diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 9b2ffe75..2575a2e9 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -274,7 +274,7 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - # NOTE: per-walker debug log loop removed — it was O(num_walkers) Python + # NOTE: per-walker debug log loop removed -- it was O(num_walkers) Python # work (np.bincount per walker) executed regardless of log level, which # at nw = 16384 added measurable startup overhead. # for i_walker in range(self.__num_walkers): @@ -488,7 +488,7 @@ def load_from_hdf5(cls, filepath: str, rank: int | None = None) -> "GFMC_t": # -- Hamiltonian data (apply DiffMask as the normal setter does) -- obj._GFMC_t__hamiltonian_data = hamiltonian_data - obj.hamiltonian_data = hamiltonian_data # triggers setter → DiffMask + __init_attributes + obj.hamiltonian_data = hamiltonian_data # triggers setter -> DiffMask + __init_attributes # -- Overwrite __init_attributes results with loaded state -- obj._GFMC_t__mcmc_counter = cfg.get("mcmc_counter", 0) @@ -2016,7 +2016,7 @@ def _run_projection_loop_streaming(pcl, tll, wll, ru, rd, Ainv, key, ks): # Each process computes the sum of its local walker weights. local_weight_sum = np.sum(w_L_latest) - # Use pickle‐based allreduce here (allowed for this part) + # Use pickle-based allreduce here (allowed for this part) global_weight_sum = mpi_comm.allreduce(local_weight_sum, op=MPI.SUM) end_ = time.perf_counter() @@ -2116,7 +2116,7 @@ def _run_projection_loop_streaming(pcl, tll, wll, ru, rd, Ainv, key, ks): # 3. Exchange only the necessary walker data between processes using asynchronous communication ######################################### - # 3.1.1: Flatten `reqs` into an (N_req × 3) int32 array of triplets + # 3.1.1: Flatten `reqs` into an (N_req x 3) int32 array of triplets start_ = time.perf_counter() flat_list = [ (src_rank, dest_idx, src_local_idx) for src_rank, pairs in reqs.items() for dest_idx, src_local_idx in pairs @@ -2176,7 +2176,7 @@ def _run_projection_loop_streaming(pcl, tll, wll, ru, rd, Ainv, key, ks): end_ = time.perf_counter() logger.devel(f" timer_reconfigration step 3.1.8 = {(end_ - start_) * 1e3:.3f} msec.") - # 3.1.9: Wait for data to arrive and reconstruct per‐process request dicts + # 3.1.9: Wait for data to arrive and reconstruct per-process request dicts start_ = time.perf_counter() all_reqs = [] for p in range(mpi_size): @@ -2466,12 +2466,12 @@ def get_E( # Two-pass jackknife std (centered sum of squares) to avoid # catastrophic cancellation in - ^2. - # E: 1st pass — mean, 2nd pass — centered sum of squares + # E: 1st pass -- mean, 2nd pass -- centered sum of squares E_mean = np.sum(E_jackknife_binned_local) / M_local E_var = np.sum((E_jackknife_binned_local - E_mean) ** 2) / M_local E_std = np.sqrt((M_local - 1) * E_var) - # Var: 1st pass — mean, 2nd pass — centered sum of squares + # Var: 1st pass -- mean, 2nd pass -- centered sum of squares Var_mean = np.sum(Var_jackknife_binned_local) / M_total Var_var = np.sum((Var_jackknife_binned_local - Var_mean) ** 2) / M_total Var_std = np.sqrt((M_total - 1) * Var_var) @@ -2560,20 +2560,20 @@ def get_E( # Two-pass jackknife std (centered sum of squares) to avoid # catastrophic cancellation in - ^2. - # E: 1st pass — global mean + # E: 1st pass -- global mean sum_E_global = mpi_comm.allreduce(np.sum(E_jackknife_binned_local), op=MPI.SUM) E_mean = sum_E_global / M_total - # E: 2nd pass — centered sum of squares (numerically stable) + # E: 2nd pass -- centered sum of squares (numerically stable) sumsq_centered_E_global = mpi_comm.allreduce(np.sum((E_jackknife_binned_local - E_mean) ** 2), op=MPI.SUM) E_var = sumsq_centered_E_global / M_total E_std = np.sqrt((M_total - 1) * E_var) - # Var: 1st pass — global mean + # Var: 1st pass -- global mean sum_Var_global = mpi_comm.allreduce(np.sum(Var_jackknife_binned_local), op=MPI.SUM) Var_mean = sum_Var_global / M_total - # Var: 2nd pass — centered sum of squares + # Var: 2nd pass -- centered sum of squares sumsq_centered_Var_global = mpi_comm.allreduce(np.sum((Var_jackknife_binned_local - Var_mean) ** 2), op=MPI.SUM) Var_var = sumsq_centered_Var_global / M_total Var_std = np.sqrt((M_total - 1) * Var_var) @@ -2857,13 +2857,13 @@ def get_aF( # Two-pass jackknife std (centered sum of squares) to avoid # catastrophic cancellation in - ^2. - # 1st pass — global mean + # 1st pass -- global mean sum_force_local = np.sum(force_jn_local, axis=0) sum_force_global = np.empty_like(sum_force_local) mpi_comm.Allreduce([sum_force_local, MPI.DOUBLE], [sum_force_global, MPI.DOUBLE], op=MPI.SUM) mean_force_global = sum_force_global / M_total - # 2nd pass — centered sum of squares (numerically stable) + # 2nd pass -- centered sum of squares (numerically stable) sumsq_centered_force_local = np.sum((force_jn_local - mean_force_global) ** 2, axis=0) sumsq_centered_force_global = np.empty_like(sumsq_centered_force_local) mpi_comm.Allreduce([sumsq_centered_force_local, MPI.DOUBLE], [sumsq_centered_force_global, MPI.DOUBLE], op=MPI.SUM) @@ -2972,7 +2972,7 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - # NOTE: per-walker debug log loop removed — it was O(num_walkers) Python + # NOTE: per-walker debug log loop removed -- it was O(num_walkers) Python # work (np.bincount per walker) executed regardless of log level, which # at nw = 16384 added measurable startup overhead. # for i_walker in range(self.__num_walkers): @@ -4284,7 +4284,7 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - # NOTE: per-walker debug log loop removed — it was O(num_walkers) Python + # NOTE: per-walker debug log loop removed -- it was O(num_walkers) Python # work (np.bincount per walker) executed regardless of log level, which # at nw = 16384 added measurable startup overhead. # for i_walker in range(self.__num_walkers): @@ -4353,7 +4353,7 @@ def __init_attributes(self): self.__stored_force_PP = np.zeros((0, 1, n_atoms, 3), dtype=dtype_np) self.__stored_E_L_force_PP = np.zeros((0, 1, n_atoms, 3), dtype=dtype_np) - # stored G_L and G_e_L for updating the E_scf (kept as lists — variable count per run) + # stored G_L and G_e_L for updating the E_scf (kept as lists -- variable count per run) self.__G_L = [] self.__G_e_L = [] @@ -4500,7 +4500,7 @@ def load_from_hdf5(cls, filepath: str, rank: int | None = None) -> "GFMC_n": # -- Hamiltonian data (apply DiffMask as the normal setter does) -- obj._GFMC_n__hamiltonian_data = hamiltonian_data - obj.hamiltonian_data = hamiltonian_data # triggers setter → DiffMask + __init_attributes + obj.hamiltonian_data = hamiltonian_data # triggers setter -> DiffMask + __init_attributes # -- Overwrite __init_attributes results with loaded state -- obj._GFMC_n__mcmc_counter = cfg.get("mcmc_counter", 0) @@ -5164,7 +5164,7 @@ def _update_inv_dn_n(_): @jit def _body_fun_n(i, carry): - """Legacy GFMC projection body — recomputes kinetic energies fresh per step.""" + """Legacy GFMC projection body -- recomputes kinetic energies fresh per step.""" ( w_L, r_up_carts, @@ -5207,7 +5207,7 @@ def _body_fun_n(i, carry): @jit def _body_fun_n_streaming(i, carry): - """Streaming GFMC projection body — reads kinetic energies from a maintained + """Streaming GFMC projection body -- reads kinetic energies from a maintained ``Kinetic_streaming_state`` (J3 incrementally; J1/J2/det fresh in PR1) and advances the state at the end of each step. @@ -5280,7 +5280,7 @@ def _split_body(current_key, _): latest_jax_PRNG_key, (rotation_keys, move_keys) = _split_step_keys(init_jax_PRNG_key, num_mcmc_per_measurement) # Python-static dispatch: the streaming path is incompatible with - # the NN three-body Jastrow (J_NN has no rank-1 advance — see + # the NN three-body Jastrow (J_NN has no rank-1 advance -- see # lrdmc_refactoring.md 1-4). When NN J3 is present, fall back to # the legacy path that recomputes kinetic energies fresh each step. # The streaming path is also compatible only when J3 is present; @@ -6066,7 +6066,7 @@ def _compute_local_energy_n( # Each process computes the sum of its local walker weights. local_weight_sum = np.sum(w_L_latest) - # Use pickle‐based allreduce here (allowed for this part) + # Use pickle-based allreduce here (allowed for this part) global_weight_sum = mpi_comm.allreduce(local_weight_sum, op=MPI.SUM) end_ = time.perf_counter() @@ -6162,7 +6162,7 @@ def _compute_local_energy_n( # 3. Exchange only the necessary walker data between processes using asynchronous communication ######################################### - # 3.1.1: Flatten `reqs` into an (N_req × 3) int32 array of triplets + # 3.1.1: Flatten `reqs` into an (N_req x 3) int32 array of triplets start_ = time.perf_counter() flat_list = [ (src_rank, dest_idx, src_local_idx) for src_rank, pairs in reqs.items() for dest_idx, src_local_idx in pairs @@ -6222,7 +6222,7 @@ def _compute_local_energy_n( end_ = time.perf_counter() logger.devel(f" timer_reconfigration step 3.1.8 = {(end_ - start_) * 1e3:.3f} msec.") - # 3.1.9: Wait for data to arrive and reconstruct per‐process request dicts + # 3.1.9: Wait for data to arrive and reconstruct per-process request dicts start_ = time.perf_counter() all_reqs = [] for p in range(mpi_size): @@ -6573,12 +6573,12 @@ def get_E( # Two-pass jackknife std (centered sum of squares) to avoid # catastrophic cancellation in - ^2. - # E: 1st pass — mean, 2nd pass — centered sum of squares + # E: 1st pass -- mean, 2nd pass -- centered sum of squares E_mean = np.sum(E_jackknife_binned_local) / M_local E_var = np.sum((E_jackknife_binned_local - E_mean) ** 2) / M_local E_std = np.sqrt((M_local - 1) * E_var) - # Var: 1st pass — mean, 2nd pass — centered sum of squares + # Var: 1st pass -- mean, 2nd pass -- centered sum of squares Var_mean = np.sum(Var_jackknife_binned_local) / M_total Var_var = np.sum((Var_jackknife_binned_local - Var_mean) ** 2) / M_total Var_std = np.sqrt((M_total - 1) * Var_var) @@ -6667,20 +6667,20 @@ def get_E( # Two-pass jackknife std (centered sum of squares) to avoid # catastrophic cancellation in - ^2. - # E: 1st pass — global mean + # E: 1st pass -- global mean sum_E_global = mpi_comm.allreduce(np.sum(E_jackknife_binned_local), op=MPI.SUM) E_mean = sum_E_global / M_total - # E: 2nd pass — centered sum of squares (numerically stable) + # E: 2nd pass -- centered sum of squares (numerically stable) sumsq_centered_E_global = mpi_comm.allreduce(np.sum((E_jackknife_binned_local - E_mean) ** 2), op=MPI.SUM) E_var = sumsq_centered_E_global / M_total E_std = np.sqrt((M_total - 1) * E_var) - # Var: 1st pass — global mean + # Var: 1st pass -- global mean sum_Var_global = mpi_comm.allreduce(np.sum(Var_jackknife_binned_local), op=MPI.SUM) Var_mean = sum_Var_global / M_total - # Var: 2nd pass — centered sum of squares + # Var: 2nd pass -- centered sum of squares sumsq_centered_Var_global = mpi_comm.allreduce(np.sum((Var_jackknife_binned_local - Var_mean) ** 2), op=MPI.SUM) Var_var = sumsq_centered_Var_global / M_total Var_std = np.sqrt((M_total - 1) * Var_var) @@ -6967,13 +6967,13 @@ def get_aF( # Two-pass jackknife std (centered sum of squares) to avoid # catastrophic cancellation in - ^2. - # 1st pass — global mean + # 1st pass -- global mean sum_force_local = np.sum(force_jn_local, axis=0) sum_force_global = np.empty_like(sum_force_local) mpi_comm.Allreduce([sum_force_local, MPI.DOUBLE], [sum_force_global, MPI.DOUBLE], op=MPI.SUM) mean_force_global = sum_force_global / M_total - # 2nd pass — centered sum of squares (numerically stable) + # 2nd pass -- centered sum of squares (numerically stable) sumsq_centered_force_local = np.sum((force_jn_local - mean_force_global) ** 2, axis=0) sumsq_centered_force_global = np.empty_like(sumsq_centered_force_local) mpi_comm.Allreduce([sumsq_centered_force_local, MPI.DOUBLE], [sumsq_centered_force_global, MPI.DOUBLE], op=MPI.SUM) @@ -7082,7 +7082,7 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - # NOTE: per-walker debug log loop removed — it was O(num_walkers) Python + # NOTE: per-walker debug log loop removed -- it was O(num_walkers) Python # work (np.bincount per walker) executed regardless of log level, which # at nw = 16384 added measurable startup overhead. # for i_walker in range(self.__num_walkers): diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index cc751793..08d7b447 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -120,7 +120,7 @@ def _loglevel_devel(self, message, *args, **kwargs): class MCMC: """Production VMC/MCMC driver with multiple walkers. - This class drives Metropolis–Hastings sampling for many independent walkers in parallel + This class drives Metropolis-Hastings sampling for many independent walkers in parallel (vectorized with ``jax.vmap``) and stores all observables needed by downstream analysis and optimization. All public methods are part of the supported API; private helpers are internal and subject to change. @@ -225,7 +225,7 @@ def __init__( ) ## Electron assignment for all atoms is complete. Check the assignment. - # NOTE: per-walker debug log loop removed — it was O(num_walkers) Python + # NOTE: per-walker debug log loop removed -- it was O(num_walkers) Python # work (np.bincount per walker) executed regardless of log level, which # at nw = 16384 added measurable startup overhead. # for i_walker in range(self.__num_walkers): @@ -446,7 +446,7 @@ def __set_optimizer_runtime( } def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: - """Execute Metropolis–Hastings sampling for all walkers. + """Execute Metropolis-Hastings sampling for all walkers. Args: num_mcmc_steps (int, optional): Metropolis updates per walker; values <= 0 are no-ops. Defaults to 0. @@ -900,7 +900,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: f" min={np.nanmin(_r_dn):.3e} max={np.nanmax(_r_dn):.3e}" ) else: - logger.devel(f" [de_L/dc] r_dn_carts: shape={_r_dn.shape} (empty — no down-spin electrons)") + logger.devel(f" [de_L/dc] r_dn_carts: shape={_r_dn.shape} (empty -- no down-spin electrons)") logger.devel(f" [de_L/dc] RTs: shape={_RTs.shape} non-finite={_nan_RTs}/{_RTs.size}") grad_e_L_h_step = _jit_vmap_grad_e_L_h( @@ -1165,20 +1165,20 @@ def get_E( # Two-pass jackknife std (centered sum of squares) to avoid catastrophic # cancellation in - ^2 when M_total grows large (many walkers). - # E: 1st pass — global mean + # E: 1st pass -- global mean sum_E_global = mpi_comm.allreduce(np.sum(E_jackknife_binned_local), op=MPI.SUM) E_mean = sum_E_global / M_total - # E: 2nd pass — centered sum of squares (numerically stable) + # E: 2nd pass -- centered sum of squares (numerically stable) sumsq_centered_E_global = mpi_comm.allreduce(np.sum((E_jackknife_binned_local - E_mean) ** 2), op=MPI.SUM) E_var = sumsq_centered_E_global / M_total E_std = np.sqrt((M_total - 1) * E_var) - # Var: 1st pass — global mean + # Var: 1st pass -- global mean sum_Var_global = mpi_comm.allreduce(np.sum(Var_jackknife_binned_local), op=MPI.SUM) Var_mean = sum_Var_global / M_total - # Var: 2nd pass — centered sum of squares + # Var: 2nd pass -- centered sum of squares sumsq_centered_Var_global = mpi_comm.allreduce(np.sum((Var_jackknife_binned_local - Var_mean) ** 2), op=MPI.SUM) Var_var = sumsq_centered_Var_global / M_total Var_std = np.sqrt((M_total - 1) * Var_var) @@ -1193,7 +1193,7 @@ def get_aF( num_mcmc_warmup_steps: int = 50, num_mcmc_bin_blocks: int = 10, ): - """Compute Hellmann–Feynman + Pulay forces with jackknife statistics. + """Compute Hellmann-Feynman + Pulay forces with jackknife statistics. Args: num_mcmc_warmup_steps (int, optional): Samples to drop for warmup. Defaults to 50. @@ -1347,13 +1347,13 @@ def get_aF( # Two-pass jackknife std (centered sum of squares) to avoid catastrophic # cancellation in - ^2 when M_total grows large. - # 1st pass — global mean + # 1st pass -- global mean sum_force_local = np.sum(force_jn_local, axis=0) sum_force_global = np.empty_like(sum_force_local) mpi_comm.Allreduce([sum_force_local, MPI.DOUBLE], [sum_force_global, MPI.DOUBLE], op=MPI.SUM) mean_force_global = sum_force_global / M_total - # 2nd pass — centered sum of squares (numerically stable) + # 2nd pass -- centered sum of squares (numerically stable) sumsq_centered_force_local = np.sum((force_jn_local - mean_force_global) ** 2, axis=0) sumsq_centered_force_global = np.empty_like(sumsq_centered_force_local) mpi_comm.Allreduce([sumsq_centered_force_local, MPI.DOUBLE], [sumsq_centered_force_global, MPI.DOUBLE], op=MPI.SUM) @@ -1427,7 +1427,7 @@ def get_dln_WF( # # Uses orthogonal-basis projectors to avoid oblique-projection amplification: # 1. Transform O_k to S^{-1/2}-orthogonalized basis - # 2. Apply orthogonal projection: Õ' = O' - (I-L') O' (I-R') + # 2. Apply orthogonal projection: O_tilde' = O' - (I-L') O' (I-R') # where L' = S^{1/2} C_up C_up^T S^{1/2}, R' = S^{1/2} C_dn C_dn^T S^{1/2} # 3. Keep O' in orthogonal basis (theta will be back-transformed later) if lambda_projectors is not None and num_orb_projection is not None: @@ -1446,7 +1446,7 @@ def get_dln_WF( # Transform paired O_k to orthogonal basis: O' = S^{-1/2}_up @ O @ S^{-1/2}_dn # Use @ with broadcasting over (m, w) batch dims instead of einsum for BLAS speed. paired_orth = inv_sqrt_overlap_up @ paired_block @ inv_sqrt_overlap_dn - # Apply orthogonal projection: Õ' = O' - (I-L') O' (I-R') + # Apply orthogonal projection: O_tilde' = O' - (I-L') O' (I-R') comp_L = identity - left_projector comp_R = identity - right_projector correction = comp_L @ paired_orth @ comp_R @@ -1666,13 +1666,13 @@ def get_gF( # Two-pass jackknife std (centered sum of squares) to avoid catastrophic # cancellation in - ^2 when M_total grows large. - # 1st pass — global mean + # 1st pass -- global mean sum_local = np.sum(force_local, axis=0) # shape (D,) sum_global = np.empty_like(sum_local) mpi_comm.Allreduce([sum_local, MPI.DOUBLE], [sum_global, MPI.DOUBLE], op=MPI.SUM) mean_global = sum_global / M_total - # 2nd pass — centered sum of squares (numerically stable) + # 2nd pass -- centered sum of squares (numerically stable) sumsq_centered_local = np.sum((force_local - mean_global) ** 2, axis=0) # shape (D,) sumsq_centered_global = np.empty_like(sumsq_centered_local) mpi_comm.Allreduce([sumsq_centered_local, MPI.DOUBLE], [sumsq_centered_global, MPI.DOUBLE], op=MPI.SUM) @@ -1748,7 +1748,7 @@ def get_aH( where :math:`\\delta\\alpha = \\gamma g` and :math:`g = S^{-1}f`. - Returns ``(H_0, H_1, H_2, S_2)`` — scalars for aSR gamma optimization. + Returns ``(H_0, H_1, H_2, S_2)`` -- scalars for aSR gamma optimization. Requires ``g`` (natural gradient direction). **LM mode** (``return_matrices=True``): @@ -1888,7 +1888,7 @@ def get_aH( dE_matrix = dE_matrix[:, :, chosen_param_index] elif chosen_param_index is not None and not (return_matrices and g is not None): dE_matrix = dE_matrix[:, :, chosen_param_index] - # else: LM fallback (no collective_obs) — keep full dE_matrix, slice later + # else: LM fallback (no collective_obs) -- keep full dE_matrix, slice later # Diagnostics: dE_matrix dE_matrix_stats = self._safe_stats(dE_matrix, "dE_matrix") @@ -1996,7 +1996,7 @@ def get_aH( # Memory-efficient path: O_SR pre-computed during SR solve. # dO_flat is already subspace-only (fetched with chosen_param_index). O_SR = collective_obs # (N_local,) - # dE_SR needs full ddE — ddE_flat is full K_full here because + # dE_SR needs full ddE -- ddE_flat is full K_full here because # dE_matrix was NOT sliced by _cpi_for_dln (see else branch below). dE_SR = ddE_flat @ g # (N,) # Slice ddE to subspace (dO_flat is already subspace) @@ -2292,7 +2292,7 @@ def solve_linear_method( H_alive = H_matrix[np.ix_(idx, idx)] f_alive = f_vec[idx] - # ---- S-orthonormalization: S = U Λ U^T, P = U Λ^{-1/2} ---- + # ---- S-orthonormalization: S = U Lambda U^T, P = U Lambda^{-1/2} ---- eigvals_S, eigvecs_S = np.linalg.eigh(S_alive) # After dgelscut, all eigenvalues should be positive, but clip for safety pos_mask = eigvals_S > 0 @@ -2304,12 +2304,12 @@ def solve_linear_method( logger.warning(" LM: no positive S eigenvalues after dgelscut; returning zero update.") return np.zeros(p, dtype=dtype_mcmc_np), H_0 - # P = U Λ^{-1/2} (S-orthonormal basis) + # P = U Lambda^{-1/2} (S-orthonormal basis) inv_sqrt_Lambda = 1.0 / np.sqrt(Lambda) P = U * inv_sqrt_Lambda[np.newaxis, :] # (n_alive, p') # Transform H and f to S-orthonormal basis - H_new = P.T @ H_alive @ P # (p', p') — should be near-identity S + H_new = P.T @ H_alive @ P # (p', p') -- should be near-identity S f_new = P.T @ f_alive # (p',) # ---- Build extended matrices (p'+1) x (p'+1) ---- @@ -2350,7 +2350,7 @@ def solve_linear_method( w0 = w[0] c_new = w[1:] / w0 # (p',) in S-orthonormal basis - # ---- Back-transform: P @ c_new → alive parameter space → full space ---- + # ---- Back-transform: P @ c_new -> alive parameter space -> full space ---- c_alive = P @ c_new # (n_alive,) c_vec = np.zeros(p, dtype=dtype_mcmc_np) c_vec[idx] = c_alive @@ -2760,8 +2760,8 @@ def _conjugate_gradient_numpy( # DEVEL: orthogonal complement-projector diagnostics (I - L') and (I - R') # ------------------------------------------------------------------ _I = np.eye(left_projector.shape[0], dtype=dtype_mcmc_np) - _comp_L = _I - left_projector # (I - L') — symmetric - _comp_R = _I - right_projector # (I - R') — symmetric + _comp_L = _I - left_projector # (I - L') -- symmetric + _comp_R = _I - right_projector # (I - R') -- symmetric # basic statistics logger.devel( @@ -2845,7 +2845,7 @@ def _conjugate_gradient_numpy( logger.info("-" * num_sep_line) logger.info("") - # Abort optimization if energy is NaN/Inf — the wavefunction is + # Abort optimization if energy is NaN/Inf -- the wavefunction is # corrupted and further updates would be meaningless. if not np.isfinite(E): logger.error( @@ -2914,7 +2914,7 @@ def _conjugate_gradient_numpy( logger.info("-" * num_sep_line) logger.info(f"Max f = {f[f_argmax]:.3f} +- {f_std[f_argmax]:.3f} Ha/a.u.") - # S/N symmetrization is no longer needed — O_k is symmetrized + # S/N symmetrization is no longer needed -- O_k is symmetrized # at source in get_dln_WF, so f and f_std are already symmetric. logger.info(f"Max of signal-to-noise of f = max(|f|/|std f|) = {np.max(signal_to_noise_f):.3f}.") @@ -3211,7 +3211,7 @@ def apply_S_primal_numpy(v): displs = [sum(counts[:i]) for i in range(P)] N_local = counts[mpi_rank] # number of rows this rank will receive - # Build send buffers by slicing X and Xw into P row‑chunks + # Build send buffers by slicing X and Xw into P row-chunks # Each chunk is flattened so we can send in one go. sendbuf_X = np.concatenate([X_local[displs[i] : displs[i] + counts[i], :].ravel() for i in range(P)]) @@ -3227,7 +3227,7 @@ def apply_S_primal_numpy(v): # Allocate receive buffers recvbuf_X = np.empty(sum(recvcounts), dtype=X_local.dtype) - # Perform the all‑to‑all variable‑sized exchange + # Perform the all-to-all variable-sized exchange mpi_comm.Alltoallv( [sendbuf_X, sendcounts, sdispls, MPI.DOUBLE], [recvbuf_X, recvcounts, rdispls, MPI.DOUBLE] ) @@ -3237,7 +3237,7 @@ def apply_S_primal_numpy(v): buf_X = recvbuf_X.reshape(P, N_local, M) # Rearrange into final 2D arrays of shape (N_local, M * P) - # by stacking each source’s M columns side by side + # by stacking each source's M columns side by side X_re_local = np.hstack([buf_X[i] for i in range(P)]) # shape (num_param/P, num_mcmc * num_walker * P) logger.devel(f"X_re_local.shape = {X_re_local.shape}.") @@ -3362,7 +3362,7 @@ def apply_dual_S_numpy(v): theta_all[_sr_frozen_mask] = 0.0 # ------------------------------------------------------------------ - # DEVEL: theta_all after scale-back — key NaN diagnosis point + # DEVEL: theta_all after scale-back -- key NaN diagnosis point # ------------------------------------------------------------------ _t_nan = int(np.sum(~np.isfinite(theta_all))) _t_fin = theta_all[np.isfinite(theta_all)] @@ -3406,7 +3406,7 @@ def apply_dual_S_numpy(v): _lm_collective_obs = None if use_lm and lm_subspace_dim != 0: dO_local = O_matrix_local - O_bar[np.newaxis, :] # (N_local, K_full) - _lm_collective_obs = dO_local @ theta_all # (N_local,) = Ō_SR per sample + _lm_collective_obs = dO_local @ theta_all # (N_local,) = O_bar_SR per sample del dO_local # free immediately # Free SR-solve temporaries that are no longer needed @@ -3522,7 +3522,7 @@ def apply_dual_S_numpy(v): ) theta = 0.1 * g_sr else: - # Back-transform: c_vec[0] = c₀ (SR direction), c_vec[1:] = c_k (individual params) + # Back-transform: c_vec[0] = c_0 (SR direction), c_vec[1:] = c_k (individual params) theta = np.zeros(total_num_params, dtype=dtype_mcmc_np) theta[:] += c_vec[0] * g_sr # SR collective variable (affects all params) if lm_subspace_dim == -1 or lm_subspace_dim >= total_num_params: @@ -3617,8 +3617,8 @@ def apply_dual_S_numpy(v): # ------------------------------------------------------------------ # 2) Back-transform theta from orthogonal basis to AO basis # for the lambda_matrix block. - # paired: θ_AO = S^{-1/2}_up @ θ'_orth @ S^{-1/2}_dn - # unpaired: θ_AO = S^{-1/2}_up @ θ'_orth + # paired: theta_AO = S^{-1/2}_up @ theta'_orth @ S^{-1/2}_dn + # unpaired: theta_AO = S^{-1/2}_up @ theta'_orth # ------------------------------------------------------------------ if lambda_projectors is not None and len(lambda_projectors) == 4: _, _, _inv_sqrt_up, _inv_sqrt_dn = lambda_projectors @@ -3689,7 +3689,7 @@ def apply_dual_S_numpy(v): block_theta = theta[start:end] if not np.any(block_theta): logger.info( - " [%s update] – block=%s size=%d theta=ALL ZERO (no update)", + " [%s update] - block=%s size=%d theta=ALL ZERO (no update)", _log_label, block.name, block.size, @@ -3699,7 +3699,7 @@ def apply_dual_S_numpy(v): block_max = float(np.max(np.abs(block_theta))) block_delta_max = float(_log_delta * block_max) logger.info( - " [%s update] – block=%s size=%d ||theta||=%.3e max|theta|=%.3e max|delta*theta|=%.3e", + " [%s update] - block=%s size=%d ||theta||=%.3e max|theta|=%.3e max|delta*theta|=%.3e", _log_label, block.name, block.size, @@ -4055,7 +4055,7 @@ def load_from_hdf5(cls, filepath: str, rank: int | None = None) -> "MCMC": # -- Hamiltonian data (apply DiffMask as the normal setter does) -- obj._MCMC__hamiltonian_data = hamiltonian_data - obj.hamiltonian_data = hamiltonian_data # triggers setter → DiffMask + __init_attributes + obj.hamiltonian_data = hamiltonian_data # triggers setter -> DiffMask + __init_attributes # -- Overwrite __init_attributes results with loaded state -- obj._MCMC__mcmc_counter = cfg.get("mcmc_counter", 0) @@ -4227,7 +4227,7 @@ def comput_e_L_param_deriv(self) -> bool: @jit def _generate_rotation_matrix(jax_PRNG_key): - """Sample a random 3×3 rotation matrix (Euler angles).""" + """Sample a random 3x3 rotation matrix (Euler angles).""" dtype_jnp = jnp.float64 _, subkey = jax.random.split(jax_PRNG_key) alpha, beta, gamma = jax.random.uniform(subkey, shape=(3,), minval=-2 * jnp.pi, maxval=2 * jnp.pi) @@ -4323,7 +4323,7 @@ def body_fun(_, carry): rand_num = jax.random.randint(subkey, shape=(), minval=0, maxval=total_electrons) # boolen: "up" or "dn" - # is_up == True -> up、False -> dn + # is_up == True -> up,False -> dn is_up = rand_num < len(r_up_carts) # an index chosen from up electons @@ -4474,9 +4474,9 @@ def body_fun(_, carry): geminal_new = lax.cond( is_up, - # Row update: row[i, :] += Δrow_i (v is (N_cols,1) -> squeeze last dim) + # Row update: row[i, :] += Deltarow_i (v is (N_cols,1) -> squeeze last dim) lambda _: geminal.at[selected_electron_index, :].add(v.squeeze(-1)), - # Column update: col[:, j] += Δcol_j (u is (N_up,1) -> squeeze last dim) + # Column update: col[:, j] += Deltacol_j (u is (N_up,1) -> squeeze last dim) lambda _: geminal.at[:, selected_electron_index].add(u.squeeze(-1)), operand=None, ) @@ -4801,7 +4801,7 @@ def __init__( self.__mpi_seed = self.__mcmc_seed * (mpi_rank + 1) self.__jax_PRNG_key = jax.random.PRNGKey(self.__mpi_seed) # Use jax.random.split (batched) to match the production MCMC class so - # MCMC ↔ _MCMC_debug parity tests stay aligned. + # MCMC <-> _MCMC_debug parity tests stay aligned. self.__jax_PRNG_key_list = jax.random.split(self.__jax_PRNG_key, self.__num_walkers) # initialize random seed @@ -4973,7 +4973,7 @@ def run(self, num_mcmc_steps: int = 0) -> None: rand_num = jax.random.randint(subkey, shape=(), minval=0, maxval=total_electrons) # boolen: "up" or "dn" - # is_up == True -> up、False -> dn + # is_up == True -> up,False -> dn is_up = rand_num < len(r_up_carts) # an index chosen from up electons @@ -5734,14 +5734,14 @@ def get_aH( return_matrices: If True, raise NotImplementedError (LM not supported in debug). Returns: - (H_0, H_1, H_2, S_2) — aSR mode only. + (H_0, H_1, H_2, S_2) -- aSR mode only. """ if not self.__comput_log_WF_param_deriv: raise RuntimeError("get_aH requires compute_log_WF_param_deriv=True.") if not self.__comput_e_L_param_deriv: raise RuntimeError("get_aH requires comput_e_L_param_deriv=True.") - # ── Step 1: Raw samples after warmup ────────────────────────────────── + # -- Step 1: Raw samples after warmup ---------------------------------- # e_L_2d, w_L_2d have shape (M_steps, num_walkers). e_L_2d = self.e_L[num_mcmc_warmup_steps:] # (M, nw) w_L_2d = self.w_L[num_mcmc_warmup_steps:] # (M, nw) @@ -5758,7 +5758,7 @@ def get_aH( e_L = e_L_2d.ravel() # (N,) w = w_L_2d.ravel() # (N,) - # ── Step 2: Build O_matrix (d ln Psi / dc) shape (N, K) ──────────── + # -- Step 2: Build O_matrix (d ln Psi / dc) shape (N, K) ------------ # dln_Psi_dc is a dict block_name -> array (M, nw, ...). # We gather blocks in the same order as `blocks` and flatten the # parameter dimensions to get a single (N, K) matrix. @@ -5787,7 +5787,7 @@ def get_aH( O_matrix[:, _sym_start:_sym_end] = block.symmetrize_metric(O_matrix[:, _sym_start:_sym_end]) _sym_start = _sym_end - # ── Step 3: Build dE_matrix (de_L / dc) shape (N, K) ─────────────── + # -- Step 3: Build dE_matrix (de_L / dc) shape (N, K) --------------- de_L_dc_map = self.de_L_dc dE_cols = [] for block in blocks: @@ -5817,7 +5817,7 @@ def get_aH( if g is not None: assert g.shape == (K_sub,), f"g shape {g.shape} != ({K_sub},)" - # ── Step 4: MPI-aware weighted averages ──────────────────────────────── + # -- Step 4: MPI-aware weighted averages -------------------------------- W_local = float(np.sum(w)) W = mpi_comm.allreduce(W_local, op=MPI.SUM) @@ -5836,21 +5836,21 @@ def get_aH( mpi_comm.Allreduce([wdE_local, MPI.DOUBLE], [wdE_global, MPI.DOUBLE], op=MPI.SUM) dE_bar = wdE_global / W - # ── Step 5: H_0 (current energy estimate) ────────────────────────── + # -- Step 5: H_0 (current energy estimate) -------------------------- H_0 = E_bar - # ── Step 6: Centered observables ────────────────────────────────────── + # -- Step 6: Centered observables -------------------------------------- dO = O_matrix - O_bar[np.newaxis, :] # (N, K) O_k(i) - ddE = dE_matrix - dE_bar[np.newaxis, :] # (N, K) dE_k(i) - - # ── Step 7: Generalized force f_k = -2/W sum_i w_i (e_L_i - E_bar) dO_{i,k} + # -- Step 7: Generalized force f_k = -2/W sum_i w_i (e_L_i - E_bar) dO_{i,k} de = e_L - E_bar # (N,) local energy fluctuation f_local = -2.0 * (w * de) @ dO # (K,) f_global = np.empty(K_sub) mpi_comm.Allreduce([f_local, MPI.DOUBLE], [f_global, MPI.DOUBLE], op=MPI.SUM) f_vec = f_global / W - # ── LM mode: build full matrices ───────────────────────────────────── + # -- LM mode: build full matrices ------------------------------------- if return_matrices: # If g (SR direction) is provided, prepend collective variable if g is not None: @@ -5888,13 +5888,13 @@ def get_aH( return H_0, f_vec, S_matrix, K_matrix, B_matrix - # ── aSR mode: scalar projections along g ───────────────────────────── + # -- aSR mode: scalar projections along g ----------------------------- assert g is not None, "g is required for aSR mode (return_matrices=False)" - # ── Step 8: H_1 = -1/2 * g^T f ────────────────────────────────────── + # -- Step 8: H_1 = -1/2 * g^T f -------------------------------------- H_1 = -0.5 * float(np.dot(g, f_vec)) - # ── Step 9: S_2 = g^T S g = _w (exact, computed from samples) ── + # -- Step 9: S_2 = g^T S g = _w (exact, computed from samples) -- # Do NOT use S_2 = g^T f (= -2*H_1). The SR solved # (S_scaled + sr_epsilon*I) g_scaled = b, so # g^T f = g^T S g + sr_epsilon * ||g_scaled||^2. @@ -5904,7 +5904,7 @@ def get_aH( S_2_local = float(np.dot(w, gdO**2)) S_2 = mpi_comm.allreduce(S_2_local, op=MPI.SUM) / W - # ── Step 10: K matrix contribution g^T K g ───────────────────────── + # -- Step 10: K matrix contribution g^T K g ------------------------- # # K_{k,k'} = 1/W sum_i w_i * e_L_i * dO_{i,k} * dO_{i,k'} # @@ -5912,7 +5912,7 @@ def get_aH( gKg_local = float(np.dot(w * e_L * gdO, gdO)) gKg = mpi_comm.allreduce(gKg_local, op=MPI.SUM) / W - # ── Step 11: B matrix contribution g^T B g ───────────────────────── + # -- Step 11: B matrix contribution g^T B g ------------------------- # # B_{k,k'} = 1/W sum_i w_i * dO_{i,k} * ddE_{i,k'} # (B is generally not symmetric) @@ -5922,7 +5922,7 @@ def get_aH( gBg_local = float(np.dot(w * gdO, gdE)) gBg = mpi_comm.allreduce(gBg_local, op=MPI.SUM) / W - # ── Step 12: H_2 = g^T (B + K) g ──────────────────────────────────── + # -- Step 12: H_2 = g^T (B + K) g ------------------------------------ H_2 = gBg + gKg return H_0, H_1, H_2, S_2 @@ -5953,7 +5953,7 @@ def solve_linear_method( Returns: (c_vec, E_lm): parameter update in original space and selected eigenvalue. """ - # Delegate to MCMC.solve_linear_method — the production version uses + # Delegate to MCMC.solve_linear_method -- the production version uses # the same dgelscut + S-orthonormalization + standard eigenvalue problem. # Duplicating the dgelscut loop in explicit form adds no clarity; # the debug value comes from get_aH (matrix construction), not the solver. diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index 3c378909..ed5607fe 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -215,7 +215,7 @@ def to_spherical(self) -> "MOs_data": """Convert Cartesian AOs to spherical AOs and transform MO coefficients. Returns a new ``MOs_data`` whose ``aos_data`` is ``AOs_sphe_data``. The molecular - orbital functions are preserved by applying the analytic Cartesian→spherical + orbital functions are preserved by applying the analytic Cartesian->spherical transformation per angular momentum shell. Only ``AOs_cart_data`` inputs are supported; for spherical inputs the instance is returned unchanged. @@ -296,7 +296,7 @@ def compute_MOs_laplacian(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) ao_lap = compute_AOs_laplacian(mos_data.aos_data, r_carts) # ao_lap lives in the ao_grad_lap zone; cast to mo_lap at the use site - # (Principle 3b — cast operands to this function's own zone immediately + # (Principle 3b -- cast operands to this function's own zone immediately # before consuming them as arithmetic operands). return jnp.dot(mo_coefficients, ao_lap.astype(dtype_jnp)) @@ -363,7 +363,7 @@ def compute_MOs_grad( mo_coefficients = mos_data._mo_coefficients_jnp.astype(dtype_jnp) mo_matrix_grad_x, mo_matrix_grad_y, mo_matrix_grad_z = compute_AOs_grad(mos_data.aos_data, r_carts) # AO gradient outputs live in the ao_grad_lap zone; cast to mo_grad at the - # use site (Principle 3b — cast operands to this function's own zone immediately + # use site (Principle 3b -- cast operands to this function's own zone immediately # before consuming them as arithmetic operands). mo_matrix_grad_x = jnp.dot(mo_coefficients, mo_matrix_grad_x.astype(dtype_jnp)) mo_matrix_grad_y = jnp.dot(mo_coefficients, mo_matrix_grad_y.astype(dtype_jnp)) @@ -379,16 +379,16 @@ def compute_MOs_value_grad_lap( """Fused evaluation of MO values, Cartesian gradients, and Laplacians. Calls :func:`compute_AOs_value_grad_lap` once and applies - ``mo_coefficients @ ·`` to each AO output. Each MO output is cast + ``mo_coefficients @ *`` to each AO output. Each MO output is cast into its own zone (Principle 3b) at the matmul use site: - * ``val`` → ``mo_eval`` (fp32 in mixed mode, fp64 in full) - * ``gx`` / ``gy`` / ``gz`` → ``mo_grad`` (fp64) - * ``lap`` → ``mo_lap`` (fp64) + * ``val`` -> ``mo_eval`` (fp32 in mixed mode, fp64 in full) + * ``gx`` / ``gy`` / ``gz`` -> ``mo_grad`` (fp64) + * ``lap`` -> ``mo_lap`` (fp64) For value-only / grad-only / lap-only call sites, prefer the standalone APIs (``compute_MOs`` / ``compute_MOs_grad`` / - ``compute_MOs_laplacian``) — JAX DCE does not reliably eliminate + ``compute_MOs_laplacian``) -- JAX DCE does not reliably eliminate unused outputs across this function's ``@jit`` boundary. Returns: diff --git a/jqmc/obsolete/qmc_kernel.py b/jqmc/obsolete/qmc_kernel.py index de64ec7e..f6b1df86 100644 --- a/jqmc/obsolete/qmc_kernel.py +++ b/jqmc/obsolete/qmc_kernel.py @@ -225,7 +225,7 @@ def __init__( # base_dn_sum = sum(base_dn_list) # 5) Compute how many extra up/down electrons are needed to reach the target totals - extra_up = tot_num_electron_up - base_up_sum # positive → need more up; negative → need more down + extra_up = tot_num_electron_up - base_up_sum # positive -> need more up; negative -> need more down # 6) Initialize final per-atom assignment lists assign_up = base_up_list.copy() @@ -572,7 +572,7 @@ def body_fun(_, carry): rand_num = jax.random.randint(subkey, shape=(), minval=0, maxval=total_electrons) # boolen: "up" or "dn" - # is_up == True -> up、False -> dn + # is_up == True -> up,False -> dn is_up = rand_num < len(r_up_carts) # an index chosen from up electons @@ -1593,7 +1593,7 @@ def __init__( # base_dn_sum = sum(base_dn_list) # 5) Compute how many extra up/down electrons are needed to reach the target totals - extra_up = tot_num_electron_up - base_up_sum # positive → need more up; negative → need more down + extra_up = tot_num_electron_up - base_up_sum # positive -> need more up; negative -> need more down # 6) Initialize final per-atom assignment lists assign_up = base_up_list.copy() @@ -2393,7 +2393,7 @@ def _projection( # Each process computes the sum of its local walker weights. local_weight_sum = np.sum(w_L_latest) - # Use pickle‐based allreduce here (allowed for this part) + # Use pickle-based allreduce here (allowed for this part) global_weight_sum = mpi_comm.allreduce(local_weight_sum, op=MPI.SUM) end_ = time.perf_counter() @@ -2496,7 +2496,7 @@ def _projection( # 3. Exchange only the necessary walker data between processes using asynchronous communication ######################################### - # 3.1.1: Flatten `reqs` into an (N_req × 3) int32 array of triplets + # 3.1.1: Flatten `reqs` into an (N_req x 3) int32 array of triplets start_ = time.perf_counter() flat_list = [ (src_rank, dest_idx, src_local_idx) for src_rank, pairs in reqs.items() for dest_idx, src_local_idx in pairs @@ -2556,7 +2556,7 @@ def _projection( end_ = time.perf_counter() logger.debug(f" reconfig: step 3.1.8 = {(end_ - start_) * 1e3:.3f} msec.") - # 3.1.9: Wait for data to arrive and reconstruct per‐process request dicts + # 3.1.9: Wait for data to arrive and reconstruct per-process request dicts start_ = time.perf_counter() all_reqs = [] for p in range(mpi_size): @@ -2861,7 +2861,7 @@ def __init__( # base_dn_sum = sum(base_dn_list) # 5) Compute how many extra up/down electrons are needed to reach the target totals - extra_up = tot_num_electron_up - base_up_sum # positive → need more up; negative → need more down + extra_up = tot_num_electron_up - base_up_sum # positive -> need more up; negative -> need more down # 6) Initialize final per-atom assignment lists assign_up = base_up_list.copy() @@ -4177,7 +4177,7 @@ def _compute_local_energy( # Each process computes the sum of its local walker weights. local_weight_sum = np.sum(w_L_latest) - # Use pickle‐based allreduce here (allowed for this part) + # Use pickle-based allreduce here (allowed for this part) global_weight_sum = mpi_comm.allreduce(local_weight_sum, op=MPI.SUM) end_ = time.perf_counter() @@ -4270,7 +4270,7 @@ def _compute_local_energy( # 3. Exchange only the necessary walker data between processes using asynchronous communication ######################################### - # 3.1.1: Flatten `reqs` into an (N_req × 3) int32 array of triplets + # 3.1.1: Flatten `reqs` into an (N_req x 3) int32 array of triplets start_ = time.perf_counter() flat_list = [ (src_rank, dest_idx, src_local_idx) for src_rank, pairs in reqs.items() for dest_idx, src_local_idx in pairs @@ -4330,7 +4330,7 @@ def _compute_local_energy( end_ = time.perf_counter() logger.debug(f" reconfig: step 3.1.8 = {(end_ - start_) * 1e3:.3f} msec.") - # 3.1.9: Wait for data to arrive and reconstruct per‐process request dicts + # 3.1.9: Wait for data to arrive and reconstruct per-process request dicts start_ = time.perf_counter() all_reqs = [] for p in range(mpi_size): @@ -5143,7 +5143,7 @@ def apply_S_primal_jax(v, X_local, epsilon): displs = [sum(counts[:i]) for i in range(P)] N_local = counts[mpi_rank] # number of rows this rank will receive - # Build send buffers by slicing X and Xw into P row‑chunks + # Build send buffers by slicing X and Xw into P row-chunks # Each chunk is flattened so we can send in one go. sendbuf_X = np.concatenate([X_local[displs[i] : displs[i] + counts[i], :].ravel() for i in range(P)]) @@ -5159,7 +5159,7 @@ def apply_S_primal_jax(v, X_local, epsilon): # Allocate receive buffers recvbuf_X = np.empty(sum(recvcounts), dtype=X_local.dtype) - # Perform the all‑to‑all variable‑sized exchange + # Perform the all-to-all variable-sized exchange mpi_comm.Alltoallv([sendbuf_X, sendcounts, sdispls, MPI.DOUBLE], [recvbuf_X, recvcounts, rdispls, MPI.DOUBLE]) # Reshape the flat receive buffer into a 3D array @@ -5167,7 +5167,7 @@ def apply_S_primal_jax(v, X_local, epsilon): buf_X = recvbuf_X.reshape(P, N_local, M) # Rearrange into final 2D arrays of shape (N_local, M * P) - # by stacking each source’s M columns side by side + # by stacking each source's M columns side by side X_re_local = np.hstack([buf_X[i] for i in range(P)]) # shape (num_param/P, num_mcmc * num_walker * P) logger.debug(f"X_re_local.shape = {X_re_local.shape}.") @@ -5233,7 +5233,7 @@ def apply_dual_S_jax(v, X_local, epsilon): # X_re_local: shape (N_local, M_total) X_re_local = jnp.array(X_re_local) # shape (M_total, N_local) - # Solve (X^T X + εI)^(-1) @ F + # Solve (X^T X + epsI)^(-1) @ F F_local_list = list(F_local) F_list = mpi_comm.allreduce(F_local_list, op=MPI.SUM) F_total = np.array(F_list) diff --git a/jqmc/obsolete/vmc_vectorized.py b/jqmc/obsolete/vmc_vectorized.py index 9e8281e6..3973b984 100644 --- a/jqmc/obsolete/vmc_vectorized.py +++ b/jqmc/obsolete/vmc_vectorized.py @@ -329,7 +329,7 @@ def _update_electron_positions(init_r_up_carts, init_r_dn_carts, jax_PRNG_key, n rand_num = jax.random.randint(subkey, shape=(), minval=0, maxval=self.__total_electrons) # boolen: "up" or "dn" - # is_up == True -> up、False -> dn + # is_up == True -> up,False -> dn is_up = rand_num < len(latest_r_up_carts) # an index chosen from up electons diff --git a/jqmc/structure.py b/jqmc/structure.py index 49eabb15..4ece9749 100755 --- a/jqmc/structure.py +++ b/jqmc/structure.py @@ -464,7 +464,7 @@ def _get_min_dist_rel_R_cart_jnp( if dtype is None: dtype = jnp.float64 # Subtract in the passed dtype (the caller chain is responsible for keeping - # high precision up to this point — see Principle 3b in jqmc._precision) + # high precision up to this point -- see Principle 3b in jqmc._precision) # and cast only the *result* down to the local zone. R_cart = structure_data._positions_cart_jnp[i_atom] diff = (R_cart - r_cart).astype(dtype) diff --git a/jqmc/swct.py b/jqmc/swct.py index 5e4cb117..d0d4741c 100644 --- a/jqmc/swct.py +++ b/jqmc/swct.py @@ -80,7 +80,7 @@ def evaluate_swct_omega( def compute_omega(R_cart, r_cart): # Reconstruct r - R in caller-supplied precision (fp64 from MCMC walker # state) via JAX promotion, then downcast to the swct zone at the use - # site (Principle 3b — cast operands to this function's own zone + # site (Principle 3b -- cast operands to this function's own zone # immediately before consuming them as arithmetic operands). diff_one = (r_cart - R_cart).astype(dtype_jnp) diff_all = (r_cart - R_carts).astype(dtype_jnp) @@ -143,7 +143,7 @@ def evaluate_swct_domega( Returns: jax.Array: Sum of gradients per atom with shape ``(N_a, 3)``. """ - # Forward r_carts as-is (Principle 3a — no parameter rebind). The inner + # Forward r_carts as-is (Principle 3a -- no parameter rebind). The inner # `evaluate_swct_omega` performs its own use-site cast to the swct zone. domega = jnp.sum(jacrev(evaluate_swct_omega, argnums=1)(structure_data, r_carts), axis=(1, 2)) diff --git a/jqmc/wavefunction.py b/jqmc/wavefunction.py index 2c5bcd56..2d5fc6fb 100644 --- a/jqmc/wavefunction.py +++ b/jqmc/wavefunction.py @@ -731,7 +731,7 @@ def evaluate_ln_wavefunction_fast( Identical to :func:`evaluate_ln_wavefunction` in the forward direction. The backward pass (used when computing :math:`\partial\ln\Psi/\partial c` via JAX autodiff) replaces the fresh LU decomposition of the geminal matrix - with ``geminal_inv`` — the Sherman-Morrison running inverse — so that + with ``geminal_inv`` -- the Sherman-Morrison running inverse -- so that near-singular configurations (``epsilon_AS > 0``) do not produce NaN gradients. @@ -751,7 +751,7 @@ def evaluate_ln_wavefunction_fast( exactly at the supplied electron positions. Correctness is only guaranteed when the inverse is maintained via **single-electron (rank-1) Sherman-Morrison updates** starting from a freshly - initialized LU inverse — the pattern used in the MCMC loop. + initialized LU inverse -- the pattern used in the MCMC loop. Passing an inverse from a different configuration silently produces incorrect parameter gradients (``O_matrix`` / SR). """ @@ -989,8 +989,8 @@ def _compute_kinetic_energy_all_elements_debug( """See compute_kinetic_energy_api. Uses 4th-order central finite differences for the Laplacian: - f''(x) ≈ (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h²) - This allows a larger step size h while maintaining accuracy (O(h⁴) truncation error). + f''(x) ~= (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h^2) + This allows a larger step size h while maintaining accuracy (O(h^4) truncation error). """ diff_h = 1.0e-3 # larger h is viable with 4th-order stencil @@ -1003,7 +1003,7 @@ def _eval_dn(r_dn): return evaluate_wavefunction(wavefunction_data, r_up_carts, r_dn) def _fd4_second_deriv(eval_fn, r_carts, i, d, h): - """4th-order central FD for d²f/dx².""" + """4th-order central FD for d^2f/dx^2.""" r_p1 = r_carts.copy() r_p2 = r_carts.copy() r_m1 = r_carts.copy() @@ -1172,7 +1172,7 @@ def compute_kinetic_energy_all_elements_fast_update( exactly at the supplied electron positions. Correctness is only guaranteed when the inverse is maintained via **single-electron (rank-1) Sherman-Morrison updates** starting from a freshly - initialized LU inverse — the pattern used in the MCMC loop. + initialized LU inverse -- the pattern used in the MCMC loop. Passing an inverse from a different configuration silently produces incorrect kinetic energy. """ @@ -1237,7 +1237,7 @@ def _compute_kinetic_energy_all_elements_fast_update_debug( # Maintains enough auxiliary information to advance the per-electron kinetic # energies after a single-electron move without recomputing them from # scratch. PR1 (devel-speedup-lrdmc-incremental) enables this only for the -# J3 part — J1, J2 and the determinant gradients/Laplacians are still +# J3 part -- J1, J2 and the determinant gradients/Laplacians are still # recomputed fresh inside ``_advance_*``. Subsequent PRs will replace those # fresh recomputes with rank-1 updates while keeping the public per-electron # fields (``grad_J_up`` etc.) shape-stable. @@ -1258,9 +1258,9 @@ class Kinetic_streaming_state: determinant per-electron grad/lap fields below mirror its outputs). - ``grad_J_up`` / ``grad_J_dn``: total Jastrow per-electron gradient. - ``lap_J_up`` / ``lap_J_dn``: total Jastrow per-electron Laplacian. - - ``grad_ln_D_up`` / ``grad_ln_D_dn``: per-electron ``∇ln|Det|`` from the + - ``grad_ln_D_up`` / ``grad_ln_D_dn``: per-electron ``nablaln|Det|`` from the geminal at the current ``A_old_inv``. - - ``lap_ln_D_up`` / ``lap_ln_D_dn``: per-electron ``∇²ln|Det|``. + - ``lap_ln_D_up`` / ``lap_ln_D_dn``: per-electron ``nabla^2ln|Det|``. """ j1_state: Jastrow_one_body_streaming_state | None = struct.field(pytree_node=True, default=None) @@ -1287,7 +1287,7 @@ def _kinetic_energy_from_grads_laps( lap_ln_D_up, lap_ln_D_dn, ): - """Common assembly: ``-(1/2) * (∇²ln Ψ + ||∇ln Ψ||²)`` per electron.""" + """Common assembly: ``-(1/2) * (nabla^2ln Psi + ||nablaln Psi||^2)`` per electron.""" dtype_jnp = get_dtype_jnp("wf_kinetic") grad_J_up = jnp.asarray(grad_J_up, dtype=dtype_jnp) grad_J_dn = jnp.asarray(grad_J_dn, dtype=dtype_jnp) @@ -1322,7 +1322,7 @@ def _init_kinetic_energy_all_elements_streaming_state( Note: ``geminal_inverse`` must be the inverse of ``G(r_up, r_dn)`` (the same invariant as :func:`compute_kinetic_energy_all_elements_fast_update`). """ - # Per-electron Jastrow grad/lap (sum of J1/J2/J3/NN parts) — used as the + # Per-electron Jastrow grad/lap (sum of J1/J2/J3/NN parts) -- used as the # initial total. Sub-states below are populated for the streaming path. grad_J_up, grad_J_dn, lap_J_up, lap_J_dn = compute_grads_and_laplacian_Jastrow_part( jastrow_data=wavefunction_data.jastrow_data, @@ -1331,7 +1331,7 @@ def _init_kinetic_energy_all_elements_streaming_state( ) # Cast totals to the jastrow_grad_lap zone so init and advance store - # ``grad_J_*`` / ``lap_J_*`` in the same dtype (Principle 3b — required + # ``grad_J_*`` / ``lap_J_*`` in the same dtype (Principle 3b -- required # for fori_loop carry-shape stability under mixed precision, where # ``advance`` reassembles the totals from streaming sub-states that # live in the jastrow_grad_lap zone). @@ -1341,7 +1341,7 @@ def _init_kinetic_energy_all_elements_streaming_state( lap_J_up = jnp.asarray(lap_J_up, dtype=dtype_jnp) lap_J_dn = jnp.asarray(lap_J_dn, dtype=dtype_jnp) - # Determinant streaming state — drives grad_ln_D_*/lap_ln_D_* fields. + # Determinant streaming state -- drives grad_ln_D_*/lap_ln_D_* fields. det_state = _init_grads_laplacian_ln_Det_streaming_state( geminal_data=wavefunction_data.geminal_data, r_up_carts=r_up_carts, @@ -1408,7 +1408,7 @@ def _advance_kinetic_energy_all_elements_streaming_state( PR1+PR2+PR3 scope: J1, J2, J3, and det sub-states are all updated incrementally. NN three-body falls back to a fresh - ``compute_grads_and_laplacian_Jastrow_part`` call (defensive — the + ``compute_grads_and_laplacian_Jastrow_part`` call (defensive -- the streaming dispatch in ``jqmc_gfmc.py`` excludes the NN case so this branch is unreachable in production). @@ -1491,7 +1491,7 @@ def _advance_kinetic_energy_all_elements_streaming_state( lap_J_up = lap_J1_up + lap_J2_up + lap_J3_up lap_J_dn = lap_J1_dn + lap_J2_dn + lap_J3_dn - # NN three-body (autodiff path) — defensive fallback. The streaming + # NN three-body (autodiff path) -- defensive fallback. The streaming # dispatch in ``jqmc_gfmc.py`` already routes NN-on cases to the legacy # body, so this branch is unreachable in production. if jastrow_data.jastrow_nn_data is not None: diff --git a/jqmc_workflow/__init__.py b/jqmc_workflow/__init__.py index aaebee30..87adba53 100644 --- a/jqmc_workflow/__init__.py +++ b/jqmc_workflow/__init__.py @@ -1,13 +1,13 @@ -"""jqmc_workflow — Automated workflow manager for jQMC calculations. +"""jqmc_workflow -- Automated workflow manager for jQMC calculations. Public API ---------- Workflow classes: - :class:`WF_Workflow` TREXIO → hamiltonian_data.h5 conversion. + :class:`WF_Workflow` TREXIO -> hamiltonian_data.h5 conversion. :class:`VMC_Workflow` Jastrow / orbital optimisation (job_type=vmc). :class:`MCMC_Workflow` VMC production sampling (job_type=mcmc). :class:`LRDMC_Workflow` Lattice-Regularized DMC (job_type=lrdmc-bra / lrdmc-tau). - :class:`LRDMC_Ext_Workflow` Multi-alat LRDMC a²→0 extrapolation. + :class:`LRDMC_Ext_Workflow` Multi-alat LRDMC a^2->0 extrapolation. Composition helpers: :class:`Workflow` Abstract base for custom workflows. diff --git a/jqmc_workflow/_error_estimator.py b/jqmc_workflow/_error_estimator.py index 0330e507..e986838c 100644 --- a/jqmc_workflow/_error_estimator.py +++ b/jqmc_workflow/_error_estimator.py @@ -3,9 +3,9 @@ Given a short pilot run and a desired target statistical error, estimates the number of measurement steps required for a production run. -The central-limit-theorem scaling σ ∝ 1/√N is used: +The central-limit-theorem scaling sigma prop 1/sqrtN is used: - N_required = ⌈ N_pilot × (σ_pilot / σ_target)² ⌉ + N_required = ceil( N_pilot x (sigma_pilot / sigma_target)^2 ) """ # Copyright (C) 2024- Kosuke Nakano @@ -127,10 +127,10 @@ def estimate_additional_steps( are needed to bring the error from *current_error* down to *target_error*. - Uses σ ∝ 1/√N: + Uses sigma prop 1/sqrtN: - N_total = ⌈ accumulated_steps × (current_error / target_error)² ⌉ - additional = N_total − accumulated_steps + N_total = ceil( accumulated_steps x (current_error / target_error)^2 ) + additional = N_total - accumulated_steps Parameters ---------- @@ -214,7 +214,7 @@ def _format_duration(seconds: float) -> str: return f"{days}d {h}h {m}m" -# ── Patterns for "Net" times in jQMC output ────────────────── +# -- Patterns for "Net" times in jQMC output ------------------ # LRDMC: "Net GFMC time without pre-compilations = 2832.326 sec." _RE_NET_GFMC = re.compile( diff --git a/jqmc_workflow/_job.py b/jqmc_workflow/_job.py index f75213c7..d3bb83fb 100644 --- a/jqmc_workflow/_job.py +++ b/jqmc_workflow/_job.py @@ -81,7 +81,7 @@ def load_queue_data(server_machine_name: str, queue_label: str) -> dict: def get_num_mpi(queue_data: dict) -> int: """Extract the number of MPI processes from a queue configuration. - Tries ``num_cores`` first, then ``mpi_per_node × nodes``. + Tries ``num_cores`` first, then ``mpi_per_node x nodes``. Defaults to 1 if neither key is present. """ if "num_cores" in queue_data: @@ -118,7 +118,7 @@ def __init__( shutil.copytree(template_dir, cfg) raise ValueError(f"Please configure {cfg} first.") - # ── Queue settings ──────────────────────────────────────── + # -- Queue settings ---------------------------------------- self.queue_label = queue_label queue_data_path = os.path.join( cfg, @@ -136,17 +136,17 @@ def __init__( except KeyError: raise KeyError(f"queue_label='{queue_label}' not found in {queue_data_path}.") - # ── Job template ────────────────────────────────────────── + # -- Job template ------------------------------------------ self.job_submission_template = self.queue_data["submit_template"] - # ── Job parameters ──────────────────────────────────────── + # -- Job parameters ---------------------------------------- self.jobname = jobname self.run_id = run_id self.input_file = input_file self.output_file = output_file self.safe_mode = safe_mode - # ── Job state ───────────────────────────────────────────── + # -- Job state --------------------------------------------- self.max_job_submit = self.queue_data.get("max_job_submit", 1000) self.job_number = None self.job_running = False @@ -154,12 +154,12 @@ def __init__( self.job_submit_date = None self.job_check_last_time = None self.job_fetch_date = None - # ── Scheduler stdout/stderr file paths (TASK 9) ────────── + # -- Scheduler stdout/stderr file paths (TASK 9) ---------- _id_suffix = f"_{run_id}" if run_id else "" self.job_stdout: str = f"job_{jobname}{_id_suffix}.o" self.job_stderr: str = f"job_{jobname}{_id_suffix}.e" - # ── Script generation ───────────────────────────────────────── + # -- Script generation ----------------------------------------- def generate_script(self, submission_script: str = "submit.sh", *, work_dir=None): """Generate job submission script from template + queue_data.toml vars. @@ -206,7 +206,7 @@ def replace_kw(lines, keyword, value): with open(script_path, "w") as f: f.writelines(lines) - # ── Job submission ──────────────────────────────────────────── + # -- Job submission -------------------------------------------- def job_submit(self, submission_script: str = "submit.sh", from_objects=None, *, work_dir=None): """Submit the job. @@ -233,7 +233,7 @@ def job_submit(self, submission_script: str = "submit.sh", from_objects=None, *, local_cwd = os.path.abspath(work_dir) if work_dir else os.path.abspath(os.getcwd()) - # ── Submit via queuing system or remote submit script ── + # -- Submit via queuing system or remote submit script -- command = f"{self.server_machine.jobsubmit} {submission_script}" if self.server_machine.machine_type == "local": @@ -272,7 +272,7 @@ def job_submit(self, submission_script: str = "submit.sh", from_objects=None, *, finally: self._close_ssh() - # ── Job checking ────────────────────────────────────────────── + # -- Job checking ---------------------------------------------- def jobcheck(self) -> bool: """Return True if the job is still running. @@ -342,7 +342,7 @@ def jobnum_check(self) -> bool: return count < self.max_job_submit - # ── Fetch results ───────────────────────────────────────────── + # -- Fetch results --------------------------------------------- def fetch_job(self, from_objects=None, exclude_patterns=None, *, work_dir=None, optional_patterns=None): """Fetch job results from the remote machine. @@ -376,21 +376,21 @@ def fetch_job(self, from_objects=None, exclude_patterns=None, *, work_dir=None, self.job_fetch_date = datetime.today() self._close_ssh() - # ── Delete a running job ────────────────────────────────────── + # -- Delete a running job -------------------------------------- def delete_job(self): self.server_machine.delete_job(jobid=self.job_number) self.job_running = False self._close_ssh() - # ── Job accounting (TASK 8) ──────────────────────────────── + # -- Job accounting (TASK 8) -------------------------------- def job_acct(self) -> tuple[str, str, str] | None: """Run the scheduler accounting command and return raw output. Reads the ``jobacct`` field from ``machine_data.yaml`` and executes ``{jobacct} {job_id}``. No parsing or flag-injection - is performed — the user specifies the complete command with + is performed -- the user specifies the complete command with flags in the config. Returns @@ -413,7 +413,7 @@ def job_acct(self) -> tuple[str, str, str] | None: logger.warning(f"job_acct failed for job {self.job_number}: {e}") return None - # ── Helper ──────────────────────────────────────────────────── + # -- Helper ---------------------------------------------------- def _close_ssh(self): self.server_machine.ssh_close() diff --git a/jqmc_workflow/_lrdmc_calibration.py b/jqmc_workflow/_lrdmc_calibration.py index 1be8e1c3..34a849b7 100644 --- a/jqmc_workflow/_lrdmc_calibration.py +++ b/jqmc_workflow/_lrdmc_calibration.py @@ -1,4 +1,4 @@ -"""LRDMC calibration utilities — survived walkers ratio. +"""LRDMC calibration utilities -- survived walkers ratio. Provides helper functions for determining the optimal ``num_projection_per_measurement`` based on a target survived-walkers ratio. @@ -56,7 +56,7 @@ logger = getLogger("jqmc-workflow").getChild(__name__) -# ── HDF5 electron count ────────────────────────────────────────── +# -- HDF5 electron count ------------------------------------------ def get_num_electrons(hamiltonian_file: str) -> int: @@ -87,7 +87,7 @@ def get_num_electrons(hamiltonian_file: str) -> int: raise RuntimeError(f"Cannot read electron counts from {hamiltonian_file}: {e}") from e -# ── Survived walkers ratio parsing ─────────────────────────────── +# -- Survived walkers ratio parsing ------------------------------- _SURVIVED_PATTERN = re.compile(r"Survived walkers ratio\s*=\s*(\d+\.?\d*)\s*%") @@ -97,7 +97,7 @@ def parse_survived_walkers_ratio(output_file: str) -> Optional[float]: Searches for the line ``Survived walkers ratio = %`` - and returns the **last** occurrence as a fraction (0.0–1.0). + and returns the **last** occurrence as a fraction (0.0-1.0). Parameters ---------- @@ -121,7 +121,7 @@ def parse_survived_walkers_ratio(output_file: str) -> Optional[float]: return last_value -# ── Linear fitting ─────────────────────────────────────────────── +# -- Linear fitting ----------------------------------------------- def fit_num_projection_per_measurement( @@ -141,7 +141,7 @@ def fit_num_projection_per_measurement( x_values : list[int] ``num_projection_per_measurement`` values used in calibration runs. y_values : list[float] - Corresponding survived-walkers ratios (fractions, 0.0–1.0). + Corresponding survived-walkers ratios (fractions, 0.0-1.0). target_ratio : float Target survived-walkers ratio (e.g. 0.97). diff --git a/jqmc_workflow/_machine.py b/jqmc_workflow/_machine.py index e2e584e4..08f74896 100644 --- a/jqmc_workflow/_machine.py +++ b/jqmc_workflow/_machine.py @@ -105,7 +105,7 @@ def __del__(self): except Exception: pass - # ── SSH management ────────────────────────────────────────────── + # -- SSH management ---------------------------------------------- @staticmethod def _kill_proxy_process(proxy_cmd): @@ -131,7 +131,7 @@ def _kill_proxy_process(proxy_cmd): pipe.close() except Exception: pass - # Kill the process (SIGKILL — SIGTERM may be ignored) + # Kill the process (SIGKILL -- SIGTERM may be ignored) try: proc.kill() except OSError: @@ -229,7 +229,7 @@ def ssh_close(self): if self.machine_type != "remote" or not self.ssh_status: return - # Save proxy reference before closing — ssh.close() may clear it + # Save proxy reference before closing -- ssh.close() may clear it proxy_cmd = self._proxy_cmd self._proxy_cmd = None @@ -241,7 +241,7 @@ def ssh_close(self): future.result(timeout=timeout_sec) logger.debug(f"{obj_name}.close() ok") except Exception as e: - logger.warning(f"{obj_name}.close() failed ({e.__class__.__name__}: {e}) — abandoning") + logger.warning(f"{obj_name}.close() failed ({e.__class__.__name__}: {e}) -- abandoning") future.cancel() finally: executor.shutdown(wait=False) @@ -258,7 +258,7 @@ def ssh_close(self): del self.sftp self.ssh_status = False - # ── Properties (read from machine_data.yaml) ────────────────── + # -- Properties (read from machine_data.yaml) ------------------ _MISSING = object() # sentinel for _get() default detection @@ -325,7 +325,7 @@ def jobdel(self) -> str: def jobnum_index(self) -> int: return self._get("jobnum_index") - # ── Command execution ───────────────────────────────────────── + # -- Command execution ----------------------------------------- def run_command(self, command: str, execute_dir: str = None): if execute_dir: @@ -404,7 +404,7 @@ def _run_remote(self, command_r: str): raise RuntimeError(f"Remote command failed (exit={exit_status}): {command_r}") return stdout, stderr - # ── Filesystem queries ──────────────────────────────────────── + # -- Filesystem queries ---------------------------------------- def _sftp_lstat_with_retry(self, path: str, max_retries=3, timeout_sec=5.0): self.ssh_open() @@ -444,7 +444,7 @@ def exist(self, object_name: str) -> bool: return False return stat.S_ISDIR(fileattr.st_mode) or stat.S_ISREG(fileattr.st_mode) - # ── Job list queries ────────────────────────────────────────── + # -- Job list queries ------------------------------------------ def get_job_list(self): return self.run_command(self.jobcheck) @@ -461,7 +461,7 @@ def delete_job(self, jobid): class Machines_handler: """Handles data transfer between localhost and a server machine. - The client is always localhost — only one Machine (server) is needed. + The client is always localhost -- only one Machine (server) is needed. """ def __init__(self, machine: Machine): @@ -470,7 +470,7 @@ def __init__(self, machine: Machine): def ssh_close(self): self.server_machine.ssh_close() - # ── put / get conveniences ──────────────────────────────────── + # -- put / get conveniences ------------------------------------ def put(self, from_file, to_file, exclude_patterns=None): self._transfer(from_file, to_file, exclude_patterns, dir_transfer=False, direction="put") @@ -484,7 +484,7 @@ def get(self, from_file, to_file, exclude_patterns=None): def get_dir(self, from_dir, to_dir, exclude_patterns=None): self._transfer(from_dir, to_dir, exclude_patterns, dir_transfer=True, direction="get") - # ── SFTP primitives ─────────────────────────────────────────── + # -- SFTP primitives ------------------------------------------- def _get_sftp_file(self, source, target, exclude_patterns): if exclude_patterns and any(re.match(p, os.path.basename(source)) for p in exclude_patterns): @@ -529,7 +529,7 @@ def _put_sftp_dir(self, source, target, exclude_patterns): elif os.path.isdir(local_path): self._put_sftp_dir(local_path, remote_path, exclude_patterns) - # ── Core transfer logic ─────────────────────────────────────── + # -- Core transfer logic --------------------------------------- def _transfer(self, from_path, to_path, exclude_patterns, dir_transfer, direction): exclude_patterns = exclude_patterns or [] @@ -564,7 +564,7 @@ def _transfer(self, from_path, to_path, exclude_patterns, dir_transfer, directio self._get_sftp_file(from_path, to_path, exclude_patterns) -# ── Machine catalog (MCP adapter helpers) ───────────────────────── +# -- Machine catalog (MCP adapter helpers) ------------------------- def list_machines() -> list[dict]: @@ -597,7 +597,7 @@ def probe_environment(machine_name: str) -> dict: For remote machines an SSH connection is attempted; for local machines reachability is always ``True``. No software detection (jqmc, JAX, etc.) - is performed — that responsibility belongs to the MCP agent. + is performed -- that responsibility belongs to the MCP agent. """ machine = Machine(machine_name) result: dict = {"machine_name": machine_name, "machine_type": machine.machine_type} diff --git a/jqmc_workflow/_output_parser.py b/jqmc_workflow/_output_parser.py index de0b4344..29f69256 100644 --- a/jqmc_workflow/_output_parser.py +++ b/jqmc_workflow/_output_parser.py @@ -2,7 +2,7 @@ This module extracts **facts only** from jQMC log files using regular expressions. It contains no heuristic judgments or convergence assessments -— those belong to a higher-level diagnostics layer (e.g. jqmc-mcp). +-- those belong to a higher-level diagnostics layer (e.g. jqmc-mcp). The parser unifies and extends the scattered parse logic previously found in ``vmc_workflow._parse_output``, ``vmc_workflow._parse_all_snr``, @@ -14,13 +14,13 @@ Public API ---------- parse_vmc_output(work_dir) - Parse VMC optimization stdout/stderr → VMC_Diagnostic_Data. + Parse VMC optimization stdout/stderr -> VMC_Diagnostic_Data. parse_mcmc_output(work_dir) - Parse MCMC sampling stdout/stderr → MCMC_Diagnostic_Data. + Parse MCMC sampling stdout/stderr -> MCMC_Diagnostic_Data. parse_lrdmc_output(work_dir) - Parse LRDMC stdout/stderr → LRDMC_Diagnostic_Data. + Parse LRDMC stdout/stderr -> LRDMC_Diagnostic_Data. parse_input_params(toml_path) - Extract key parameters from a TOML input file → Input_Parameters. + Extract key parameters from a TOML input file -> Input_Parameters. """ # Copyright (C) 2024- Kosuke Nakano @@ -77,17 +77,17 @@ logger = getLogger("jqmc-workflow").getChild(__name__) -# ── Atomic-force table parser ───────────────────────────────────── +# -- Atomic-force table parser ------------------------------------- def parse_ufloat_short(text: str): - """Parse uncertainties short format, e.g. ``+0.123(45)`` → (0.123, 0.045). + """Parse uncertainties short format, e.g. ``+0.123(45)`` -> (0.123, 0.045). Handles several notations produced by jqmc: - * ``+0.0114(14)`` — integer uncertainty digits in last decimal place - * ``+3(8)e-05`` — scientific notation with integer uncertainty - * ``+3.9(3.5)e-05`` — scientific notation with decimal uncertainty + * ``+0.0114(14)`` -- integer uncertainty digits in last decimal place + * ``+3(8)e-05`` -- scientific notation with integer uncertainty + * ``+3.9(3.5)e-05`` -- scientific notation with decimal uncertainty Parameters ---------- @@ -226,7 +226,7 @@ def repair_forces_from_output(work_dir: str) -> bool: return True -# ── Compiled regex patterns ─────────────────────────────────────── +# -- Compiled regex patterns --------------------------------------- # # All patterns are compiled once at module level for efficiency. @@ -311,7 +311,7 @@ def repair_forces_from_output(work_dir: str) -> bool: # "Pre-compilation time for GFMC = 167.674 sec." _RE_PRECOMP_GFMC = re.compile(r"Pre-compilation time for GFMC\s*=\s*([\d.]+(?:[eE][+-]?\d+)?)\s*sec") -# Per-branching timing breakdown (msec) — GFMC +# Per-branching timing breakdown (msec) -- GFMC _RE_TIME_PROJECTION = re.compile( r"Projection(?:\s+time per branching|\s+between branching)\s*=\s*([\d.]+(?:[eE][+-]?\d+)?)\s*msec" ) @@ -341,7 +341,7 @@ def repair_forces_from_output(work_dir: str) -> bool: # "Dump restart checkpoint file(s) to restart.h5." _RE_RESTART_CHECKPOINT = re.compile(r"Dump restart checkpoint file\(s\) to\s+(\S+)\.\s*$", re.MULTILINE) -# ── Run-level metadata (header section, appears once per output file) ── +# -- Run-level metadata (header section, appears once per output file) -- # "The number of MPI process = 4." _RE_MPI_PROCESSES = re.compile(r"The number of MPI process\s*=\s*(\d+)") @@ -361,7 +361,7 @@ def repair_forces_from_output(work_dir: str) -> bool: _RE_XLA_DEVICE_LIST = re.compile(r"\[([^\]]+)\]") -# ── Internal helpers ────────────────────────────────────────────── +# -- Internal helpers ---------------------------------------------- def _read_text(path: str) -> Optional[str]: @@ -469,7 +469,7 @@ def _parse_run_metadata(text: str) -> dict: meta["jax_backend"] = "cpu" continue - # XLA Global devices — parse from the full text (may span lines) + # XLA Global devices -- parse from the full text (may span lines) m_header = _RE_XLA_GLOBAL_HEADER.search(text) if m_header: # The device list is on the next non-empty line after the header @@ -520,7 +520,7 @@ def _find_output_files(work_dir: str) -> list: return [path for _, path in files] -# ── VMC parser ──────────────────────────────────────────────────── +# -- VMC parser ---------------------------------------------------- def _parse_vmc_log_text(text: str) -> list: @@ -553,7 +553,7 @@ def _parse_vmc_log_text(text: str) -> list: total_opt_steps: Optional[int] = None for line in text.splitlines(): - # ── Optimization step header ── + # -- Optimization step header -- m = _RE_OPT_STEP.search(line) if m: step_num = int(m.group(1)) @@ -565,25 +565,25 @@ def _parse_vmc_log_text(text: str) -> list: if current is None: continue - # ── Net MCMC time ── + # -- Net MCMC time -- m = _RE_NET_MCMC.search(line) if m: current.net_time_sec = float(m.group(1)) continue - # ── Total MCMC time ── + # -- Total MCMC time -- m = _RE_TOTAL_MCMC.search(line) if m: current.total_time_sec = float(m.group(1)) continue - # ── Pre-compilation MCMC time ── + # -- Pre-compilation MCMC time -- m = _RE_PRECOMP_MCMC.search(line) if m: current.precompilation_time_sec = float(m.group(1)) continue - # ── Per-step MCMC timing breakdown (msec) ── + # -- Per-step MCMC timing breakdown (msec) -- m = _RE_TIME_MCMC_UPDATE.search(line) if m: current.timing_breakdown["mcmc_update"] = float(m.group(1)) @@ -617,32 +617,32 @@ def _parse_vmc_log_text(text: str) -> list: current.timing_breakdown["misc"] = float(m.group(1)) continue - # ── Walker weight ── + # -- Walker weight -- m = _RE_WALKER_WEIGHT.search(line) if m: current.avg_walker_weight = float(m.group(1)) continue - # ── Acceptance ratio ── + # -- Acceptance ratio -- m = _RE_ACCEPTANCE.search(line) if m: current.acceptance_ratio = float(m.group(1)) / 100.0 continue - # ── Signal-to-noise (must be checked BEFORE energy) ── + # -- Signal-to-noise (must be checked BEFORE energy) -- m = _RE_SNR.search(line) if m: current.signal_to_noise_ratio = float(m.group(1)) continue - # ── Max force (must be checked BEFORE energy) ── + # -- Max force (must be checked BEFORE energy) -- m = _RE_MAX_FORCE.search(line) if m: current.max_force = float(m.group(1)) current.max_force_error = float(m.group(2)) continue - # ── Energy ── + # -- Energy -- m = _RE_ENERGY.search(line) if m: current.energy = float(m.group(1)) @@ -675,7 +675,7 @@ def parse_vmc_output(work_dir: str) -> VMC_Diagnostic_Data: logger.warning("parse_vmc_output: directory not found: %s", work_dir) return result - # ── Discover and parse stdout files ── + # -- Discover and parse stdout files -- output_files = _find_output_files(work_dir) all_steps: list[VMC_Step_Data] = [] @@ -688,7 +688,7 @@ def parse_vmc_output(work_dir: str) -> VMC_Diagnostic_Data: result.steps = all_steps - # ── Run-level metadata (MPI, walkers, JAX) from first output file ── + # -- Run-level metadata (MPI, walkers, JAX) from first output file -- if output_files: first_text = _read_text(output_files[0]) if first_text: @@ -703,7 +703,7 @@ def parse_vmc_output(work_dir: str) -> VMC_Diagnostic_Data: if m: result.total_opt_steps = int(m.group(2)) - # ── Optimization-level timing (appears once after all steps) ── + # -- Optimization-level timing (appears once after all steps) -- m = _RE_TOTAL_OPT.search(last_text) if m: result.total_opt_time_sec = float(m.group(1)) @@ -722,7 +722,7 @@ def parse_vmc_output(work_dir: str) -> VMC_Diagnostic_Data: if m_bd: result.opt_timing_breakdown[key] = float(m_bd.group(1)) - # ── optimized hamiltonian ── + # -- optimized hamiltonian -- # Find hamiltonian_data_opt_step_*.h5 and sort numerically by step. h5_pattern = os.path.join(work_dir, "hamiltonian_data_opt_step_*.h5") h5_files = glob.glob(h5_pattern) @@ -740,7 +740,7 @@ def _h5_step_num(path: str) -> int: work_dir, ) - # ── restart checkpoint ── + # -- restart checkpoint -- # Search all output files for the last "Dump restart checkpoint" line. for fpath in reversed(output_files): text = _read_text(fpath) @@ -750,7 +750,7 @@ def _h5_step_num(path: str) -> int: if result.restart_checkpoint is not None: break - # ── stderr tail ── + # -- stderr tail -- stderr_candidates = [ os.path.join(work_dir, "stderr"), os.path.join(work_dir, "err"), @@ -765,7 +765,7 @@ def _h5_step_num(path: str) -> int: return result -# ── MCMC parser ─────────────────────────────────────────────────── +# -- MCMC parser --------------------------------------------------- def parse_mcmc_output(work_dir: str) -> MCMC_Diagnostic_Data: @@ -794,7 +794,7 @@ def parse_mcmc_output(work_dir: str) -> MCMC_Diagnostic_Data: output_files = _find_output_files(work_dir) - # ── Run-level metadata (MPI, walkers, JAX) from first output file ── + # -- Run-level metadata (MPI, walkers, JAX) from first output file -- if output_files: first_text = _read_text(output_files[0]) if first_text: @@ -809,27 +809,27 @@ def parse_mcmc_output(work_dir: str) -> MCMC_Diagnostic_Data: break if last_text: - # Walker weight — take the last occurrence + # Walker weight -- take the last occurrence for m in _RE_WALKER_WEIGHT.finditer(last_text): result.avg_walker_weight = float(m.group(1)) - # Acceptance ratio — take the last occurrence + # Acceptance ratio -- take the last occurrence for m in _RE_ACCEPTANCE.finditer(last_text): result.acceptance_ratio = float(m.group(1)) / 100.0 - # Total time — take the last occurrence + # Total time -- take the last occurrence for m in _RE_TOTAL_MCMC.finditer(last_text): result.total_time_sec = float(m.group(1)) - # Pre-compilation time — take the last occurrence + # Pre-compilation time -- take the last occurrence for m in _RE_PRECOMP_MCMC.finditer(last_text): result.precompilation_time_sec = float(m.group(1)) - # Net time — take the last occurrence + # Net time -- take the last occurrence for m in _RE_NET_MCMC.finditer(last_text): result.net_time_sec = float(m.group(1)) - # Per-step timing breakdown (msec) — take last occurrence of each + # Per-step timing breakdown (msec) -- take last occurrence of each _mcmc_breakdown_patterns = [ (_RE_TIME_MCMC_UPDATE, "mcmc_update"), (_RE_TIME_E_L, "e_L"), @@ -844,14 +844,14 @@ def parse_mcmc_output(work_dir: str) -> MCMC_Diagnostic_Data: for m in pattern.finditer(last_text): result.timing_breakdown[key] = float(m.group(1)) - # Restart checkpoint — take the last occurrence + # Restart checkpoint -- take the last occurrence for m in _RE_RESTART_CHECKPOINT.finditer(last_text): result.restart_checkpoint = m.group(1) - # ── hamiltonian_data_file from input TOML ── + # -- hamiltonian_data_file from input TOML -- result.hamiltonian_data_file = _find_hamiltonian_h5(work_dir) - # ── Energy & forces from workflow_state.toml result section ── + # -- Energy & forces from workflow_state.toml result section -- state_path = os.path.join(work_dir, "workflow_state.toml") if os.path.isfile(state_path): try: @@ -866,7 +866,7 @@ def parse_mcmc_output(work_dir: str) -> MCMC_Diagnostic_Data: except Exception: pass - # ── stderr tail ── + # -- stderr tail -- for name in ("stderr", "err"): text = _read_text(os.path.join(work_dir, name)) if text: @@ -876,7 +876,7 @@ def parse_mcmc_output(work_dir: str) -> MCMC_Diagnostic_Data: return result -# ── LRDMC parser ────────────────────────────────────────────────── +# -- LRDMC parser -------------------------------------------------- def parse_lrdmc_output(work_dir: str) -> LRDMC_Diagnostic_Data: @@ -904,7 +904,7 @@ def parse_lrdmc_output(work_dir: str) -> LRDMC_Diagnostic_Data: output_files = _find_output_files(work_dir) - # ── Run-level metadata (MPI, walkers, JAX) from first output file ── + # -- Run-level metadata (MPI, walkers, JAX) from first output file -- if output_files: first_text = _read_text(output_files[0]) if first_text: @@ -919,27 +919,27 @@ def parse_lrdmc_output(work_dir: str) -> LRDMC_Diagnostic_Data: break if last_text: - # Survived walkers ratio — take the last occurrence + # Survived walkers ratio -- take the last occurrence for m in _RE_SURVIVED.finditer(last_text): result.survived_walkers_ratio = float(m.group(1)) / 100.0 - # Average number of projections — take the last occurrence + # Average number of projections -- take the last occurrence for m in _RE_AVG_PROJECTIONS.finditer(last_text): result.avg_num_projections = float(m.group(1)) - # Total GFMC time — take the last occurrence + # Total GFMC time -- take the last occurrence for m in _RE_TOTAL_GFMC.finditer(last_text): result.total_time_sec = float(m.group(1)) - # Pre-compilation GFMC time — take the last occurrence + # Pre-compilation GFMC time -- take the last occurrence for m in _RE_PRECOMP_GFMC.finditer(last_text): result.precompilation_time_sec = float(m.group(1)) - # Net GFMC time — take the last occurrence + # Net GFMC time -- take the last occurrence for m in _RE_NET_GFMC.finditer(last_text): result.net_time_sec = float(m.group(1)) - # Per-branching timing breakdown (msec) — take last occurrence of each + # Per-branching timing breakdown (msec) -- take last occurrence of each _gfmc_breakdown_patterns = [ (_RE_TIME_PROJECTION, "projection"), (_RE_TIME_OBSERVABLE, "observable"), @@ -958,14 +958,14 @@ def parse_lrdmc_output(work_dir: str) -> LRDMC_Diagnostic_Data: for m in pattern.finditer(last_text): result.timing_breakdown[key] = float(m.group(1)) - # Restart checkpoint — take the last occurrence + # Restart checkpoint -- take the last occurrence for m in _RE_RESTART_CHECKPOINT.finditer(last_text): result.restart_checkpoint = m.group(1) - # ── hamiltonian_data_file from input TOML ── + # -- hamiltonian_data_file from input TOML -- result.hamiltonian_data_file = _find_hamiltonian_h5(work_dir) - # ── Energy & forces from workflow_state.toml result section ── + # -- Energy & forces from workflow_state.toml result section -- state_path = os.path.join(work_dir, "workflow_state.toml") if os.path.isfile(state_path): try: @@ -980,7 +980,7 @@ def parse_lrdmc_output(work_dir: str) -> LRDMC_Diagnostic_Data: except Exception: pass - # ── stderr tail ── + # -- stderr tail -- for name in ("stderr", "err"): text = _read_text(os.path.join(work_dir, name)) if text: @@ -990,7 +990,7 @@ def parse_lrdmc_output(work_dir: str) -> LRDMC_Diagnostic_Data: return result -# ── LRDMC extrapolation parser ──────────────────────────────────── +# -- LRDMC extrapolation parser ------------------------------------ def parse_lrdmc_ext_output(work_dir: str) -> LRDMC_Ext_Diagnostic_Data: @@ -1027,7 +1027,7 @@ def parse_lrdmc_ext_output(work_dir: str) -> LRDMC_Ext_Diagnostic_Data: result.extrapolated_energy_error = float(m.group(2)) break - # ── per-alat results from workflow_state.toml ── + # -- per-alat results from workflow_state.toml -- state_path = os.path.join(work_dir, "workflow_state.toml") if os.path.isfile(state_path): try: @@ -1038,7 +1038,7 @@ def parse_lrdmc_ext_output(work_dir: str) -> LRDMC_Ext_Diagnostic_Data: except Exception: pass - # ── stderr tail ── + # -- stderr tail -- for name in ("stderr", "err"): text = _read_text(os.path.join(work_dir, name)) if text: @@ -1048,7 +1048,7 @@ def parse_lrdmc_ext_output(work_dir: str) -> LRDMC_Ext_Diagnostic_Data: return result -# ── Input parameters parser ────────────────────────────────────── +# -- Input parameters parser -------------------------------------- def _get_cli_defaults() -> dict: @@ -1094,7 +1094,7 @@ def parse_input_params(work_dir: str) -> Input_Parameters: logger.warning("parse_input_params: directory not found: %s", work_dir) return result - # ── 1) restart.h5 → actual_opt_steps (VMC only) ── + # -- 1) restart.h5 -> actual_opt_steps (VMC only) -- restart_path = os.path.join(work_dir, "restart.h5") if os.path.isfile(restart_path): try: @@ -1113,7 +1113,7 @@ def parse_input_params(work_dir: str) -> Input_Parameters: exc, ) - # ── 2) workflow_state.toml → [[jobs]] ── + # -- 2) workflow_state.toml -> [[jobs]] -- state_path = os.path.join(work_dir, "workflow_state.toml") jobs: list = [] if os.path.isfile(state_path): @@ -1123,7 +1123,7 @@ def parse_input_params(work_dir: str) -> Input_Parameters: except Exception: pass - # ── 3) Per-input parameter extraction ── + # -- 3) Per-input parameter extraction -- defaults = _get_cli_defaults() for job_rec in jobs: @@ -1159,7 +1159,7 @@ def parse_input_params(work_dir: str) -> Input_Parameters: jt_merged = {**jt_defaults, **jt_user} entry[job_type] = jt_merged elif job_type: - # No defaults known — just use raw values + # No defaults known -- just use raw values entry[job_type] = raw.get(job_type, {}) result.per_input.append(entry) diff --git a/jqmc_workflow/_phase.py b/jqmc_workflow/_phase.py index ad4cd71f..312d20a9 100644 --- a/jqmc_workflow/_phase.py +++ b/jqmc_workflow/_phase.py @@ -1,7 +1,7 @@ """Scientific phase definitions and transition rules. Each QMC workflow session progresses through a sequence of scientific phases -(SCF → wavefunction build → VMC → MCMC → LRDMC → fit). This module defines +(SCF -> wavefunction build -> VMC -> MCMC -> LRDMC -> fit). This module defines the allowed phase transitions and the actions permitted in each phase/status combination. @@ -241,7 +241,7 @@ def require_action( ) -> None: """Raise :class:`ValueError` if *action* is not allowed in *phase*/*status*. - Call this at the entry of every MCP-tool → workflow-method boundary to + Call this at the entry of every MCP-tool -> workflow-method boundary to enforce the guard-rail. """ allowed = allowed_actions(phase, status) diff --git a/jqmc_workflow/_results.py b/jqmc_workflow/_results.py index 6176a3c4..111da304 100644 --- a/jqmc_workflow/_results.py +++ b/jqmc_workflow/_results.py @@ -1,6 +1,6 @@ """Structured result types for jQMC output parsing. -These dataclasses represent **facts only** — deterministic data extracted +These dataclasses represent **facts only** -- deterministic data extracted from jQMC stdout/stderr and associated files. They contain no heuristic judgments; diagnostic analysis (failure categorization, convergence assessment, etc.) belongs to a higher-level layer (e.g. jqmc-mcp). @@ -58,7 +58,7 @@ from dataclasses import dataclass, field from typing import Optional -# ── VMC ─────────────────────────────────────────────────────────── +# -- VMC ----------------------------------------------------------- @dataclass @@ -68,21 +68,21 @@ class VMC_Step_Data: Attributes ---------- step : int - Optimization step number (``Optimization step = N/M`` → N). + Optimization step number (``Optimization step = N/M`` -> N). energy : float or None - Total energy ``E = X +- Y`` → X (Ha). + Total energy ``E = X +- Y`` -> X (Ha). energy_error : float or None - Energy statistical error → Y (Ha). + Energy statistical error -> Y (Ha). max_force : float or None - Maximum force ``Max f = X +- Y`` → X (Ha/a.u.). + Maximum force ``Max f = X +- Y`` -> X (Ha/a.u.). max_force_error : float or None - Force error → Y (Ha/a.u.). + Force error -> Y (Ha/a.u.). signal_to_noise_ratio : float or None ``Max of signal-to-noise of f = max(|f|/|std f|) = X``. avg_walker_weight : float or None ``Average of walker weights is X``. acceptance_ratio : float or None - ``Acceptance ratio is X %`` → X / 100. + ``Acceptance ratio is X %`` -> X / 100. total_time_sec : float or None ``Total elapsed time for MCMC N steps. = X sec.`` precompilation_time_sec : float or None @@ -117,7 +117,7 @@ class VMC_Diagnostic_Data: steps : list of VMC_Step_Data Per-step data in chronological order. total_opt_steps : int or None - Total optimization steps (``Optimization step = N/M`` → M). + Total optimization steps (``Optimization step = N/M`` -> M). total_opt_time_sec : float or None ``Total elapsed time for optimization N steps. = X sec.`` opt_timing_breakdown : dict @@ -130,11 +130,11 @@ class VMC_Diagnostic_Data: Restart file name from ``Dump restart checkpoint file(s) to X.``. ``None`` if the line was not found (indicates abnormal termination). num_mpi_processes : int or None - ``The number of MPI process = N.`` → N. + ``The number of MPI process = N.`` -> N. num_walkers_per_process : int or None - ``The number of walkers assigned for each MPI process = N.`` → N. + ``The number of walkers assigned for each MPI process = N.`` -> N. jax_backend : str or None - ``JAX backend = X.`` → X (e.g. ``"gpu"``, ``"cpu"``). + ``JAX backend = X.`` -> X (e.g. ``"gpu"``, ``"cpu"``). Set to ``"cpu"`` when the log says ``Running on CPUs or single GPU``. jax_devices : list or None @@ -158,7 +158,7 @@ class VMC_Diagnostic_Data: stderr_tail: str = "" -# ── MCMC ────────────────────────────────────────────────────────── +# -- MCMC ---------------------------------------------------------- @dataclass @@ -168,7 +168,7 @@ class MCMC_Diagnostic_Data: Attributes ---------- acceptance_ratio : float or None - ``Acceptance ratio is X %`` → X / 100. + ``Acceptance ratio is X %`` -> X / 100. avg_walker_weight : float or None ``Average of walker weights is X``. total_time_sec : float or None @@ -194,11 +194,11 @@ class MCMC_Diagnostic_Data: Restart file name from ``Dump restart checkpoint file(s) to X.``. ``None`` if the line was not found. num_mpi_processes : int or None - ``The number of MPI process = N.`` → N. + ``The number of MPI process = N.`` -> N. num_walkers_per_process : int or None - ``The number of walkers assigned for each MPI process = N.`` → N. + ``The number of walkers assigned for each MPI process = N.`` -> N. jax_backend : str or None - ``JAX backend = X.`` → X (e.g. ``"gpu"``, ``"cpu"``). + ``JAX backend = X.`` -> X (e.g. ``"gpu"``, ``"cpu"``). jax_devices : list or None Parsed list of global XLA device strings. stderr_tail : str @@ -223,7 +223,7 @@ class MCMC_Diagnostic_Data: stderr_tail: str = "" -# ── LRDMC ───────────────────────────────────────────────────────── +# -- LRDMC --------------------------------------------------------- @dataclass @@ -233,7 +233,7 @@ class LRDMC_Diagnostic_Data: Attributes ---------- survived_walkers_ratio : float or None - ``Survived walkers ratio = X %`` → X / 100. + ``Survived walkers ratio = X %`` -> X / 100. avg_num_projections : float or None ``Average of the number of projections = X``. total_time_sec : float or None @@ -260,11 +260,11 @@ class LRDMC_Diagnostic_Data: Restart file name from ``Dump restart checkpoint file(s) to X.``. ``None`` if the line was not found. num_mpi_processes : int or None - ``The number of MPI process = N.`` → N. + ``The number of MPI process = N.`` -> N. num_walkers_per_process : int or None - ``The number of walkers assigned for each MPI process = N.`` → N. + ``The number of walkers assigned for each MPI process = N.`` -> N. jax_backend : str or None - ``JAX backend = X.`` → X (e.g. ``"gpu"``, ``"cpu"``). + ``JAX backend = X.`` -> X (e.g. ``"gpu"``, ``"cpu"``). jax_devices : list or None Parsed list of global XLA device strings. stderr_tail : str @@ -289,17 +289,17 @@ class LRDMC_Diagnostic_Data: stderr_tail: str = "" -# ── LRDMC extrapolation ────────────────────────────────────────── +# -- LRDMC extrapolation ------------------------------------------ @dataclass class LRDMC_Ext_Diagnostic_Data: - """Parse result for an LRDMC a²→0 extrapolation. + """Parse result for an LRDMC a^2->0 extrapolation. Attributes ---------- extrapolated_energy : float or None - ``For a -> 0 bohr: E = X +- Y Ha.`` → X. + ``For a -> 0 bohr: E = X +- Y Ha.`` -> X. extrapolated_energy_error : float or None Y from the above. per_alat_results : list of dict @@ -314,7 +314,7 @@ class LRDMC_Ext_Diagnostic_Data: stderr_tail: str = "" -# ── Input parameters ────────────────────────────────────────────── +# -- Input parameters ---------------------------------------------- @dataclass diff --git a/jqmc_workflow/_state.py b/jqmc_workflow/_state.py index ae24fe6f..80ddbfbf 100644 --- a/jqmc_workflow/_state.py +++ b/jqmc_workflow/_state.py @@ -87,9 +87,9 @@ class CompletionStatus(str, Enum): Single source of truth for "should the workflow terminate?". - ``OK`` — all checks pass (converged or post-hoc validation). - ``FAILED`` — irrecoverable: abnormal termination, non-finite energy. - ``INCOMPLETE`` — no failure signal, but convergence criterion not yet + ``OK`` -- all checks pass (converged or post-hoc validation). + ``FAILED`` -- irrecoverable: abnormal termination, non-finite energy. + ``INCOMPLETE`` -- no failure signal, but convergence criterion not yet met (only meaningful when ``target_error`` is given). """ @@ -98,7 +98,7 @@ class CompletionStatus(str, Enum): INCOMPLETE = "incomplete" -# Legacy sets — kept for backward compatibility during transition. +# Legacy sets -- kept for backward compatibility during transition. # New code should use WorkflowStatus / JobStatus enums. VALID_STATUSES = {s.value for s in WorkflowStatus} VALID_JOB_STATUSES = {s.value for s in JobStatus} @@ -169,7 +169,7 @@ def _check_normal_termination(directory: str, jobs: list) -> list[str]: """Check fetched output files for the ``Program ends`` marker. Returns a list of output-file names that exist on disk but do **not** - contain the ``Program ends`` line — a strong signal that the + contain the ``Program ends`` line -- a strong signal that the computation was killed (e.g. wall-time expiration) before normal termination. @@ -182,7 +182,7 @@ def _check_normal_termination(directory: str, jobs: list) -> list[str]: continue filepath = os.path.join(directory, output_file) if not os.path.isfile(filepath): - continue # not fetched yet — nothing to check + continue # not fetched yet -- nothing to check try: with open(filepath, "r", errors="replace") as f: # Read only the tail (last 8 KiB) for efficiency; @@ -194,7 +194,7 @@ def _check_normal_termination(directory: str, jobs: list) -> list[str]: if "Program ends" not in tail: abnormal.append(output_file) except OSError: - continue # unreadable — skip + continue # unreadable -- skip return abnormal @@ -209,11 +209,11 @@ def validate_completion( Used in two modes: - * **Post-hoc validation** (``target_error=None``) — called once by + * **Post-hoc validation** (``target_error=None``) -- called once by :class:`Container` after a workflow reports ``COMPLETED``. Only irrecoverable failures are detected; returns ``OK`` or ``FAILED``. - * **Per-iteration check** (``target_error`` given) — called inside + * **Per-iteration check** (``target_error`` given) -- called inside a production loop after each continuation run. May additionally return ``INCOMPLETE`` when no failure is detected but the statistical error still exceeds ``target_error * target_tol``. @@ -221,11 +221,11 @@ def validate_completion( Checks (in order; short-circuits on first failure): 1. ``Program ends`` marker missing in any fetched output file - → ``FAILED`` (e.g. wall-time kill, process crash). - 2. Non-finite energy in ``output_values`` → ``FAILED``. - 3. (target_error mode) ``energy`` not yet recorded → ``INCOMPLETE``. + -> ``FAILED`` (e.g. wall-time kill, process crash). + 2. Non-finite energy in ``output_values`` -> ``FAILED``. + 3. (target_error mode) ``energy`` not yet recorded -> ``INCOMPLETE``. 4. (target_error mode) ``energy_error > target_error * target_tol`` - → ``INCOMPLETE``; otherwise → ``OK``. + -> ``INCOMPLETE``; otherwise -> ``OK``. Parameters ---------- @@ -253,7 +253,7 @@ def validate_completion( output_values = output_values or {} state = read_state(directory) - # ── Check 1: "Program ends" marker in output files ──────────── + # -- Check 1: "Program ends" marker in output files ------------ abnormal = _check_normal_termination(directory, state.get("jobs", [])) if abnormal: files_str = ", ".join(abnormal) @@ -262,14 +262,14 @@ def validate_completion( f"Abnormal termination: 'Program ends' marker missing in output file(s): {files_str}", ) - # ── Check 2: Non-finite energy ──────────────────────────────── + # -- Check 2: Non-finite energy -------------------------------- import math energy = output_values.get("energy") if energy is not None and not math.isfinite(energy): return CompletionStatus.FAILED, f"Non-finite energy detected (E={energy})" - # ── Check 3/4: Convergence (only in per-iteration mode) ─────── + # -- Check 3/4: Convergence (only in per-iteration mode) ------- if target_error is not None: if energy is None: return ( @@ -341,7 +341,7 @@ def update_status( return state -# ── Job history (replaces old set_job_info / get_job_info) ──────── +# -- Job history (replaces old set_job_info / get_job_info) -------- def add_job( @@ -445,7 +445,7 @@ def get_jobs(directory: str) -> list: return state.get("jobs", []) -# ── Result / estimation helpers (unchanged) ─────────────────────── +# -- Result / estimation helpers (unchanged) ----------------------- def set_result(directory: str, **result_fields): @@ -486,17 +486,17 @@ def get_all_workflow_statuses(base_dir: str) -> list: Returns a list of dicts, each containing: - - ``directory`` – absolute path to the workflow directory - - ``label`` – workflow label (from ``[workflow]``) - - ``type`` – workflow type (e.g. ``"vmc"``) - - ``status`` – current workflow status + - ``directory`` - absolute path to the workflow directory + - ``label`` - workflow label (from ``[workflow]``) + - ``type`` - workflow type (e.g. ``"vmc"``) + - ``status`` - current workflow status Directories without a ``workflow_state.toml`` are silently skipped. """ results = [] base_dir = os.path.abspath(base_dir) for dirpath, dirnames, filenames in os.walk(base_dir): - # Skip pilot-run subdirectories — they have workflow_state.toml + # Skip pilot-run subdirectories -- they have workflow_state.toml # but are internal bookkeeping, not user-facing workflows. dirnames[:] = [d for d in dirnames if not d.startswith("_pilot")] if STATE_FILENAME in filenames: @@ -520,16 +520,16 @@ def get_workflow_summary(directory: str) -> dict: The returned dict contains: - - ``workflow`` – label, type, status, timestamps - - ``phase`` – current scientific phase (str or ``"init"``) - - ``allowed_actions`` – list of permitted MCP actions - - ``result`` – any stored results (energy, etc.) - - ``estimation`` – step-estimation data (if present) - - ``jobs`` – list of job records (each includes accounting + - ``workflow`` - label, type, status, timestamps + - ``phase`` - current scientific phase (str or ``"init"``) + - ``allowed_actions`` - list of permitted MCP actions + - ``result`` - any stored results (energy, etc.) + - ``estimation`` - step-estimation data (if present) + - ``jobs`` - list of job records (each includes accounting and scheduler file info when available) - - ``num_jobs`` – total number of job records - - ``error`` – ``[error]`` section or ``None`` - - ``artifacts`` – ``[[artifacts]]`` list + - ``num_jobs`` - total number of job records + - ``error`` - ``[error]`` section or ``None`` + - ``artifacts`` - ``[[artifacts]]`` list Returns an empty dict if no ``workflow_state.toml`` is found. """ @@ -562,7 +562,7 @@ def get_workflow_summary(directory: str) -> dict: } -# ── Error / accounting / artifact helpers ───────────────────────── +# -- Error / accounting / artifact helpers ------------------------- def set_error(directory: str, message: str, **context) -> None: @@ -573,7 +573,7 @@ def set_error(directory: str, message: str, **context) -> None: message : str Human-readable error description (exception message, etc.). **context - Arbitrary extra fields (``traceback``, ``exception_type``, …). + Arbitrary extra fields (``traceback``, ``exception_type``, ...). """ state = read_state(directory) state["error"] = {"message": message, **context} diff --git a/jqmc_workflow/_transfer.py b/jqmc_workflow/_transfer.py index 8222bb42..4f31b8e4 100644 --- a/jqmc_workflow/_transfer.py +++ b/jqmc_workflow/_transfer.py @@ -46,7 +46,7 @@ class Data_transfer: - """Convenience layer over Machines_handler for local ↔ remote transfers. + """Convenience layer over Machines_handler for local <-> remote transfers. Parameters ---------- @@ -79,7 +79,7 @@ def ssh_close(self): self.server_machine.ssh_close() self.machine_handler.ssh_close() - # ── put (local → remote) ────────────────────────────────────── + # -- put (local -> remote) -------------------------------------- def put_objects(self, from_objects=None, exclude_patterns=None, *, work_dir=None): """Upload files from *work_dir* to the corresponding remote directory. @@ -153,7 +153,7 @@ def put_objects(self, from_objects=None, exclude_patterns=None, *, work_dir=None exclude_patterns=exclude_patterns, ) - # ── get (remote → local) ────────────────────────────────────── + # -- get (remote -> local) -------------------------------------- def get_objects(self, from_objects=None, exclude_patterns=None, *, work_dir=None, optional_patterns=None): """Download files from the remote directory to *work_dir*. @@ -240,12 +240,12 @@ def get_objects(self, from_objects=None, exclude_patterns=None, *, work_dir=None exclude_patterns=exclude_patterns, ) - # ── remove (local + remote) ────────────────────────────────── + # -- remove (local + remote) ---------------------------------- def remove_objects(self, patterns: list[str], *, work_dir: str | None = None) -> None: """Delete files matching *patterns* from local and (if remote) server. - Matching is **recursive** — each pattern is applied to *work_dir* + Matching is **recursive** -- each pattern is applied to *work_dir* and all of its subdirectories (e.g. ``_pilot/``, ``_pilot_a/``). Parameters @@ -259,7 +259,7 @@ def remove_objects(self, patterns: list[str], *, work_dir: str | None = None) -> """ local_cwd = os.path.abspath(work_dir) if work_dir else os.path.abspath(os.getcwd()) - # ── Local deletion (always, recursive) ─────────────────── + # -- Local deletion (always, recursive) ------------------- for pattern in patterns: for fpath in sorted(glob.glob(os.path.join(local_cwd, "**", pattern), recursive=True)): if os.path.isfile(fpath): @@ -267,7 +267,7 @@ def remove_objects(self, patterns: list[str], *, work_dir: str | None = None) -> relpath = os.path.relpath(fpath, local_cwd) logger.info(f" Cleanup: removed local file {relpath}") - # ── Remote deletion (only for non-local machines) ──────── + # -- Remote deletion (only for non-local machines) -------- if self.server_machine.machine_type == "local": return diff --git a/jqmc_workflow/launcher.py b/jqmc_workflow/launcher.py index c6e2f3b2..05b52445 100644 --- a/jqmc_workflow/launcher.py +++ b/jqmc_workflow/launcher.py @@ -1,7 +1,7 @@ """Launcher: DAG-based parallel workflow executor for jqmc-workflow. True DAG execution: as soon as ALL predecessors of a node complete, -that node starts immediately — no waiting for the entire "layer". +that node starts immediately -- no waiting for the entire "layer". Supports FileFrom / ValueFrom dependencies. """ @@ -68,7 +68,7 @@ class Launcher: infers the dependency graph from :class:`FileFrom` / :class:`ValueFrom` references, and executes workflows with *true DAG parallelism*: as soon as **all** predecessors of a node complete, that node starts immediately - — there is no layer-based grouping. + -- there is no layer-based grouping. Parameters ---------- @@ -157,20 +157,20 @@ def __init__( ): workflows = workflows or [] - # ── Logger setup ────────────────────────────────────────── + # -- Logger setup ------------------------------------------ self._setup_logger(log_level, log_name) from ._header_footer import _print_header _print_header() - # ── Resolve config dir early (CWD is still the user dir) ── + # -- Resolve config dir early (CWD is still the user dir) -- from ._config import get_config_dir _cfg = get_config_dir() logger.debug(f"Config dir resolved to: {_cfg}") - # ── Attributes ──────────────────────────────────────────── + # -- Attributes -------------------------------------------- self.root_dir = os.getcwd() self.workflows = workflows @@ -201,7 +201,7 @@ def __init__( logger.info("-" * 50) logger.info("") - # ── Logger setup ────────────────────────────────────────────── + # -- Logger setup ---------------------------------------------- def _setup_logger(self, log_level: str, log_name: str): global _loggers_initialized @@ -229,7 +229,7 @@ def _setup_logger(self, log_level: str, log_name: str): _loggers_initialized[name] = True - # ── Dependency graph ────────────────────────────────────────── + # -- Dependency graph ------------------------------------------ def _build_dependency_graph(self) -> dict: """Walk all workflow attributes to find dependency placeholders.""" @@ -240,7 +240,7 @@ def _build_dependency_graph(self) -> dict: self._collect_deps(cw, dep_labels) dep_dict[cw.label] = tuple(dep_labels) - # Validate — all dependency labels must exist + # Validate -- all dependency labels must exist all_labels = set(self.workflows_by_label.keys()) for label, deps in dep_dict.items(): missing = set(deps) - all_labels @@ -293,7 +293,7 @@ def _draw_graph(self): G.render("dependency_graph", cleanup=True) logger.info("Dependency graph saved to dependency_graph.png") - # ── Session / job queries (MCP adapter layer) ─────────────── + # -- Session / job queries (MCP adapter layer) --------------- def get_session_state(self) -> dict: """Aggregate the status of all workflows, dependency graph, and progress. @@ -359,7 +359,7 @@ def get_job_history(self) -> list[dict]: history.sort(key=lambda j: j.get("submitted_at", "")) return history - # ── Variable resolution ─────────────────────────────────────── + # -- Variable resolution --------------------------------------- def _get_value(self, dep_obj): """Resolve a FileFrom / ValueFrom to its actual value.""" @@ -412,7 +412,7 @@ def _resolve_obj(self, obj): elif isinstance(value, Workflow): self._resolve_obj(value) - # ── Execution: true DAG parallelism ─────────────────────────── + # -- Execution: true DAG parallelism --------------------------- def launch(self): asyncio.run(self.async_launch()) @@ -421,7 +421,7 @@ async def async_launch(self): """Execute all workflows respecting DAG dependencies. As soon as ALL predecessors of a node complete, that node - starts immediately — no layer-based grouping. + starts immediately -- no layer-based grouping. """ completed = set() failed = set() diff --git a/jqmc_workflow/lrdmc_ext_workflow.py b/jqmc_workflow/lrdmc_ext_workflow.py index 5268134e..d0862181 100644 --- a/jqmc_workflow/lrdmc_ext_workflow.py +++ b/jqmc_workflow/lrdmc_ext_workflow.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""LRDMC_Ext_Workflow — LRDMC extrapolation to the a²→0 limit. +"""LRDMC_Ext_Workflow -- LRDMC extrapolation to the a^2->0 limit. Orchestrates multiple :class:`LRDMC_Workflow` runs at different lattice spacings (``alat`` values), then post-processes with @@ -62,7 +62,7 @@ class LRDMC_Ext_Workflow(Workflow): - """LRDMC a²→0 continuum-limit extrapolation workflow. + """LRDMC a^2->0 continuum-limit extrapolation workflow. Orchestrates multiple :class:`LRDMC_Workflow` runs at different lattice spacings (``alat`` values), then post-processes with @@ -78,8 +78,8 @@ class LRDMC_Ext_Workflow(Workflow): **Mode selection** follows the same rules as :class:`LRDMC_Workflow`: - * **GFMC_t** (default) — set *time_projection_tau* (default 0.10). - * **GFMC_n** — set *target_survived_walkers_ratio* or + * **GFMC_t** (default) -- set *time_projection_tau* (default 0.10). + * **GFMC_n** -- set *target_survived_walkers_ratio* or *num_projection_per_measurement*. Parameters @@ -104,7 +104,7 @@ class LRDMC_Ext_Workflow(Workflow): max_time : int Wall-time limit per sub-run (seconds). polynomial_order : int - Polynomial order for the a²→0 extrapolation (default: 2). + Polynomial order for the a^2->0 extrapolation (default: 2). num_gfmc_bin_blocks : int Binning blocks for post-processing. num_gfmc_warmup_steps : int @@ -136,7 +136,7 @@ class LRDMC_Ext_Workflow(Workflow): Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. Default is False for LRDMC. epsilon_PW : float, optional - Pathak–Wagner regularization parameter (Bohr). When > 0, + Pathak-Wagner regularization parameter (Bohr). When > 0, the force estimator is regularized near the nodal surface. Default from ``jqmc_miscs``. mcmc_seed : int, optional @@ -207,7 +207,7 @@ class LRDMC_Ext_Workflow(Workflow): After ``launch()`` completes, ``output_values`` may contain: extrapolated_energy : float - Continuum-limit (a²→0) extrapolated energy (Ha). + Continuum-limit (a^2->0) extrapolated energy (Ha). extrapolated_energy_error : float Statistical error on ``extrapolated_energy`` (Ha). per_alat_results : dict @@ -293,9 +293,9 @@ def __init__( self.time_projection_tau = time_projection_tau self.target_survived_walkers_ratio = target_survived_walkers_ratio # num_projection_per_measurement may be: - # None — GFMC_t mode (uses time_projection_tau) - # int — same value for every alat - # dict — per-alat values; keys must cover every alat in alat_list + # None -- GFMC_t mode (uses time_projection_tau) + # int -- same value for every alat + # dict -- per-alat values; keys must cover every alat in alat_list if isinstance(num_projection_per_measurement, dict): missing = [a for a in self.alat_list if a not in num_projection_per_measurement] if missing: @@ -395,7 +395,7 @@ def configure(self) -> dict: } async def run(self) -> tuple: - """Run LRDMC at each alat, then extrapolate to a²→0. + """Run LRDMC at each alat, then extrapolate to a^2->0. Every ``alat`` value is launched in parallel. Each child :class:`LRDMC_Workflow` independently handles its own @@ -470,7 +470,7 @@ async def _run_one(enc): logger.info(f"All {len(self.alat_list)} LRDMC runs completed.") - # ── Extrapolation ───────────────────────────────────────── + # -- Extrapolation ----------------------------------------- if len(restart_chks) >= 2: ext_energy, ext_error = self._extrapolate_energy(restart_chks) if ext_energy is not None: diff --git a/jqmc_workflow/lrdmc_workflow.py b/jqmc_workflow/lrdmc_workflow.py index 09bd38ce..c66d5d8b 100644 --- a/jqmc_workflow/lrdmc_workflow.py +++ b/jqmc_workflow/lrdmc_workflow.py @@ -1,4 +1,4 @@ -"""LRDMC_Workflow — Lattice-Regularized Diffusion Monte Carlo run. +"""LRDMC_Workflow -- Lattice-Regularized Diffusion Monte Carlo run. Generates an LRDMC input TOML, submits ``jqmc`` (job_type=lrdmc-bra or job_type=lrdmc-tau) on a remote/local machine, monitors until completion, @@ -7,10 +7,10 @@ Two operating modes are available: -* **GFMC_n mode** (``job_type=lrdmc-bra``) — activated when +* **GFMC_n mode** (``job_type=lrdmc-bra``) -- activated when *target_survived_walkers_ratio* or *num_projection_per_measurement* is set. Uses discrete projections per measurement. -* **GFMC_t mode** (``job_type=lrdmc-tau``) — activated when +* **GFMC_t mode** (``job_type=lrdmc-tau``) -- activated when *time_projection_tau* is used (default). Uses a continuous imaginary time step between projections. No calibration pilot is needed. """ @@ -95,14 +95,14 @@ class LRDMC_Workflow(Workflow): (GFMC_t) input TOML at a fixed lattice spacing ``alat``, submits ``jqmc``, monitors until completion, fetches the checkpoint, and post-processes with ``jqmc-tool lrdmc compute-energy`` to extract - the DMC energy ± error. + the DMC energy +/- error. **Mode selection** (mutually exclusive): - * **GFMC_t** (default) — set *time_projection_tau* (default 0.10). + * **GFMC_t** (default) -- set *time_projection_tau* (default 0.10). Uses continuous imaginary-time projection. Only the error-bar pilot is run (no calibration phase). - * **GFMC_n** — set *target_survived_walkers_ratio* or + * **GFMC_n** -- set *target_survived_walkers_ratio* or *num_projection_per_measurement*. Uses discrete GFMC projections. When *target_survived_walkers_ratio* is set (and *num_projection_per_measurement* is *None*), an automatic calibration @@ -112,14 +112,14 @@ class LRDMC_Workflow(Workflow): **Automatic mode** (default, ``num_gfmc_projections=None``): - 1. **Pilot run** (``_0``) — A short run with ``pilot_steps`` + 1. **Pilot run** (``_0``) -- A short run with ``pilot_steps`` measurement steps. The resulting error estimates the steps required for ``target_error`` via $\sigma \propto 1/\sqrt{N}$. In GFMC_n mode with calibration, three additional short runs precede this to determine *num_projection_per_measurement*. - 2. **Production runs** (``_1``, ``_2``, …) — Continuation runs + 2. **Production runs** (``_1``, ``_2``, ...) -- Continuation runs with the estimated step count. The loop terminates when the - error is ≤ ``target_error`` or ``max_continuation`` is reached. + error is <= ``target_error`` or ``max_continuation`` is reached. **Fixed-step mode** (``num_gfmc_projections`` is set): @@ -161,7 +161,7 @@ class LRDMC_Workflow(Workflow): Target survived-walkers ratio for automatic ``num_projection_per_measurement`` calibration. Setting this activates GFMC_n mode. The pilot phase runs three short - calculations at ``Ne*k*(0.3/alat)²`` projections (k=2,4,6), + calculations at ``Ne*k*(0.3/alat)^2`` projections (k=2,4,6), fits a linear model to the observed survived-walkers ratio, and picks the value that achieves this target. num_projection_per_measurement : int, optional @@ -178,7 +178,7 @@ class LRDMC_Workflow(Workflow): Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. Default is False for LRDMC. epsilon_PW : float, optional - Pathak–Wagner regularization parameter (Bohr). When > 0, + Pathak-Wagner regularization parameter (Bohr). When > 0, the force estimator is regularized near the nodal surface. Default from ``jqmc_miscs``. mcmc_seed : int, optional @@ -280,7 +280,7 @@ class LRDMC_Workflow(Workflow): Notes ----- - * For a²→0 continuum-limit extrapolation, use + * For a^2->0 continuum-limit extrapolation, use :class:`LRDMC_Ext_Workflow` instead. * The pilot is skipped on re-entrance if an estimation already exists in ``workflow_state.toml``. @@ -380,7 +380,7 @@ def job_type(self) -> str: """Return the jqmc job type string for TOML generation.""" return "lrdmc-bra" if self._use_gfmc_n else "lrdmc-tau" - # ── Input generation ────────────────────────────────────────── + # -- Input generation ------------------------------------------ def _generate_input( self, @@ -470,10 +470,10 @@ def _generate_input( filename=input_file, ) - # ── Submit / poll / fetch ───────────────────────────────────── + # -- Submit / poll / fetch ------------------------------------- # _submit_and_wait() and _make_job() are inherited from Workflow. - # ── configure / run ────────────────────────────────────────── + # -- configure / run ------------------------------------------ def configure(self) -> dict: """Validate parameters and return configuration summary.""" @@ -500,21 +500,21 @@ async def run(self) -> tuple: **Automatic mode** (``num_gfmc_projections`` is *None*, default): - 1. Calibration pilot (``_pilot_a``, GFMC_n only) — Three short + 1. Calibration pilot (``_pilot_a``, GFMC_n only) -- Three short LRDMC runs to determine ``num_projection_per_measurement``. - 2. Error-bar pilot (``_pilot_b``) — estimates production steps. - 3. Production runs (``_1``, ``_2``, …) — accumulate statistics + 2. Error-bar pilot (``_pilot_b``) -- estimates production steps. + 3. Production runs (``_1``, ``_2``, ...) -- accumulate statistics until ``target_error`` is achieved or ``max_continuation`` is reached. """ self._ensure_project_dir() _wd = self.project_dir - # ── Fixed-step mode ─────────────────────────────────────── + # -- Fixed-step mode --------------------------------------- if self.num_gfmc_projections is not None: return await self._launch_fixed_steps(_wd) - # ── Automatic mode (pilot + target_error) ───────────────── + # -- Automatic mode (pilot + target_error) ----------------- return await self._launch_auto(_wd) async def _launch_fixed_steps(self, _wd): @@ -525,7 +525,7 @@ async def _launch_fixed_steps(self, _wd): """ estimated_steps = self.num_gfmc_projections - # ── Phase A: calibrate num_projection_per_measurement (GFMC_n only) ── + # -- Phase A: calibrate num_projection_per_measurement (GFMC_n only) -- need_calibration = ( self._use_gfmc_n and self.num_projection_per_measurement is None and self.target_survived_walkers_ratio is not None ) @@ -617,7 +617,7 @@ async def _launch_fixed_steps(self, _wd): last_num_gfmc_collect_steps=self.num_gfmc_collect_steps, ) - # ── Abnormal-termination guard (single source of truth) ── + # -- Abnormal-termination guard (single source of truth) -- # Fixed-step mode has no convergence criterion, so only the # Program-ends / non-finite-energy checks are active here. vstatus, vmsg = validate_completion(_wd, self.output_values) @@ -627,7 +627,7 @@ async def _launch_fixed_steps(self, _wd): self.status = WorkflowStatus.FAILED break - # ── Final energy computation ───────────────────────────── + # -- Final energy computation ----------------------------- last_output = step_files[last_run][1] if last_run in step_files else None restart_chk = self._find_restart_chk(_wd) if restart_chk: @@ -650,7 +650,7 @@ async def _launch_fixed_steps(self, _wd): last_num_gfmc_collect_steps=self.num_gfmc_collect_steps, ) - # ── Collect outputs ─────────────────────────────────────── + # -- Collect outputs --------------------------------------- chk_files = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.h5"))) output_logs = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.out"))) self.output_files = chk_files + output_logs @@ -770,7 +770,7 @@ async def _launch_auto(self, _wd): f"Estimation already done (continuation): estimated_steps={estimated_steps}, {mode_str}. Skipping pilot." ) else: - # ── Phase A: calibrate num_projection_per_measurement (GFMC_n only) ── + # -- Phase A: calibrate num_projection_per_measurement (GFMC_n only) -- h5_src = os.path.join(_wd, self.hamiltonian_file) need_calibration = ( @@ -782,7 +782,7 @@ async def _launch_auto(self, _wd): if need_calibration: await self._run_calibration(_wd) - # ── Phase B: error-bar pilot (_pilot_b) ─────────────── + # -- Phase B: error-bar pilot (_pilot_b) --------------- pilot_b_dir = os.path.join(_wd, "_pilot_b") os.makedirs(pilot_b_dir, exist_ok=True) h5_link_b = os.path.join(pilot_b_dir, self.hamiltonian_file) @@ -892,7 +892,7 @@ async def _launch_auto(self, _wd): est_kwargs["time_projection_tau"] = self.time_projection_tau set_estimation(_wd, **est_kwargs) - # ── Re-compute energy if post-processing parameters changed ── + # -- Re-compute energy if post-processing parameters changed -- _postproc_changed = ( estimation.get("last_num_gfmc_bin_blocks") != self.num_gfmc_bin_blocks or estimation.get("last_num_gfmc_warmup_steps") != self.num_gfmc_warmup_steps @@ -923,7 +923,7 @@ async def _launch_auto(self, _wd): ) estimation = get_estimation(_wd) - # ── Early exit if target already met ────────────────────── + # -- Early exit if target already met ---------------------- cached_energy = estimation.get("last_energy") cached_error = estimation.get("last_energy_error") if cached_energy is not None and cached_error is not None: @@ -948,9 +948,9 @@ async def _launch_auto(self, _wd): self.status = WorkflowStatus.COMPLETED return self.status, self.output_files, self.output_values - # ── Production runs (phase 1..N) ────────────────────────── + # -- Production runs (phase 1..N) -------------------------- # Three phases: - # A. Scan existing runs → find resume point + # A. Scan existing runs -> find resume point # B. Re-estimate from accumulated data (if resuming) # C. Production loop for remaining runs # @@ -964,7 +964,7 @@ async def _launch_auto(self, _wd): last_run = 0 first_new_run = self.max_continuation + 1 # assume all done - # ── Phase A: scan existing runs ── + # -- Phase A: scan existing runs -- for i in range(1, self.max_continuation + 1): recorded = get_job_by_step(_wd, i) status_i = recorded.get("status") @@ -976,7 +976,7 @@ async def _launch_auto(self, _wd): first_new_run = i break - # ── Phase B: re-estimate from accumulated data ── + # -- Phase B: re-estimate from accumulated data -- accumulated_measurement = 0 # measurement steps only (excl. warmup) if first_new_run > 1: cached_accum = estimation.get("accumulated_measurement_steps") @@ -1020,7 +1020,7 @@ async def _launch_auto(self, _wd): f"accumulated measurement: {accumulated_measurement})" ) - # ── Phase C: production loop ── + # -- Phase C: production loop -- _prev_run_steps = None for i in range(first_new_run, self.max_continuation + 1): recorded = get_job_by_step(_wd, i) @@ -1092,7 +1092,7 @@ async def _launch_auto(self, _wd): _prev_run_steps = estimated_steps last_run = i - # ── Side-effects: compute energy from checkpoint (if any) ── + # -- Side-effects: compute energy from checkpoint (if any) -- restart_chk = self._find_restart_chk(_wd) energy = error = None if restart_chk: @@ -1118,7 +1118,7 @@ async def _launch_auto(self, _wd): last_num_gfmc_collect_steps=self.num_gfmc_collect_steps, ) - # ── Termination decision — single source of truth ── + # -- Termination decision -- single source of truth -- vstatus, vmsg = validate_completion( _wd, self.output_values, @@ -1133,7 +1133,7 @@ async def _launch_auto(self, _wd): if vstatus == CompletionStatus.OK: logger.info(f" Target error achieved: {vmsg} (run {i}/{self.max_continuation})") break - # INCOMPLETE — prepare next iteration if we have an error estimate + # INCOMPLETE -- prepare next iteration if we have an error estimate if energy is not None: if i < self.max_continuation: old_steps = estimated_steps @@ -1155,7 +1155,7 @@ async def _launch_auto(self, _wd): f"max_continuation ({self.max_continuation}) reached" ) - # ── Final energy computation (safety net) ───────────────── + # -- Final energy computation (safety net) ----------------- last_output = step_files[last_run][1] if last_run in step_files else None restart_chk = self._find_restart_chk(_wd) if restart_chk: @@ -1179,7 +1179,7 @@ async def _launch_auto(self, _wd): last_num_gfmc_collect_steps=self.num_gfmc_collect_steps, ) - # ── Collect outputs ─────────────────────────────────────── + # -- Collect outputs --------------------------------------- chk_files = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.h5"))) output_logs = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.out"))) self.output_files = chk_files + output_logs @@ -1193,7 +1193,7 @@ async def _launch_auto(self, _wd): self.status = WorkflowStatus.COMPLETED return self.status, self.output_files, self.output_values - # ── Utility methods ─────────────────────────────────────────── + # -- Utility methods ------------------------------------------- def _find_restart_chk(self, work_dir: str) -> Optional[str]: """Locate the LRDMC restart checkpoint file in *work_dir*.""" @@ -1207,10 +1207,10 @@ def _compute_energy(self, restart_chk: str, work_dir: str, output_file: Optional """Parse energy from *output_file* or run ``jqmc-tool lrdmc compute-energy``. When *output_file* is given the energy is read directly from - the ``jqmc`` stdout (``Total Energy: E = … +- … Ha.``). + the ``jqmc`` stdout (``Total Energy: E = ... +- ... Ha.``). This avoids the overhead of re-running ``jqmc-tool`` when the post-processing parameters (-b, -w, -c) are the same as - in the input TOML — which is always the case for a fresh run. + in the input TOML -- which is always the case for a fresh run. Falls back to ``jqmc-tool`` when *output_file* is *None* or when stdout parsing fails. diff --git a/jqmc_workflow/mcmc_workflow.py b/jqmc_workflow/mcmc_workflow.py index 780b7d64..b8b05203 100644 --- a/jqmc_workflow/mcmc_workflow.py +++ b/jqmc_workflow/mcmc_workflow.py @@ -1,8 +1,8 @@ -"""MCMC_Workflow — MCMC production run (sampling) via ``jqmc`` (job_type=mcmc). +"""MCMC_Workflow -- MCMC production run (sampling) via ``jqmc`` (job_type=mcmc). Generates an MCMC input TOML, submits ``jqmc`` on a remote/local machine, monitors until completion, fetches results, and post-processes the checkpoint -with ``jqmc-tool mcmc compute-energy`` to extract the VMC energy ± error. +with ``jqmc-tool mcmc compute-energy`` to extract the VMC energy +/- error. """ # Copyright (C) 2024- Kosuke Nakano @@ -73,17 +73,17 @@ class MCMC_Workflow(Workflow): Generates a ``job_type=mcmc`` input TOML, submits ``jqmc`` on a remote or local machine, monitors until completion, fetches results, and post-processes the checkpoint with - ``jqmc-tool mcmc compute-energy`` to obtain the VMC energy ± error. + ``jqmc-tool mcmc compute-energy`` to obtain the VMC energy +/- error. The workflow supports two modes: **Automatic mode** (default, ``num_mcmc_steps=None``): - 1. **Pilot run** (``_0``) — A short MCMC run with ``pilot_steps`` + 1. **Pilot run** (``_0``) -- A short MCMC run with ``pilot_steps`` measurement steps. The resulting statistical error is used to estimate the total steps required for ``target_error`` via the CLT scaling $\\sigma \\propto 1/\\sqrt{N}$. - 2. **Production runs** (``_1``, ``_2``, …) — Continuation runs + 2. **Production runs** (``_1``, ``_2``, ...) -- Continuation runs with the estimated step count. After each run, the checkpoint is post-processed; if the error is at or below ``target_error`` the loop terminates. At most ``max_continuation`` production @@ -279,7 +279,7 @@ def __init__( # [precision] section self.precision_mode = precision_mode - # ── Input generation ────────────────────────────────────────── + # -- Input generation ------------------------------------------ def _generate_input( self, @@ -328,10 +328,10 @@ def _generate_input( filename=input_file, ) - # ── Submit / poll / fetch ───────────────────────────────────── + # -- Submit / poll / fetch ------------------------------------- # _submit_and_wait() and _make_job() are inherited from Workflow. - # ── configure / run ────────────────────────────────────────── + # -- configure / run ------------------------------------------ def configure(self) -> dict: """Validate parameters and return configuration summary.""" @@ -368,11 +368,11 @@ async def run(self) -> tuple: self._ensure_project_dir() _wd = self.project_dir - # ── Fixed-step mode ─────────────────────────────────────── + # -- Fixed-step mode --------------------------------------- if self.num_mcmc_steps is not None: return await self._launch_fixed_steps(_wd) - # ── Automatic mode (pilot + target_error) ───────────────── + # -- Automatic mode (pilot + target_error) ----------------- return await self._launch_auto(_wd) async def _launch_fixed_steps(self, _wd): @@ -454,7 +454,7 @@ async def _launch_fixed_steps(self, _wd): last_num_mcmc_warmup_steps=self.num_mcmc_warmup_steps, ) - # ── Abnormal-termination guard (single source of truth) ── + # -- Abnormal-termination guard (single source of truth) -- # Fixed-step mode has no convergence criterion, so only the # Program-ends / non-finite-energy checks are active here. vstatus, vmsg = validate_completion(_wd, self.output_values) @@ -464,7 +464,7 @@ async def _launch_fixed_steps(self, _wd): self.status = WorkflowStatus.FAILED break - # ── Final energy computation ───────────────────────────── + # -- Final energy computation ----------------------------- last_output = step_files[last_run][1] if last_run in step_files else None restart_chk = self._find_restart_chk(_wd) if restart_chk: @@ -485,7 +485,7 @@ async def _launch_fixed_steps(self, _wd): last_num_mcmc_warmup_steps=self.num_mcmc_warmup_steps, ) - # ── Collect outputs ─────────────────────────────────────── + # -- Collect outputs --------------------------------------- chk_files = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.h5"))) output_logs = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.out"))) self.output_files = chk_files + output_logs @@ -504,7 +504,7 @@ async def _launch_auto(self, _wd): estimated_steps = int(estimation["estimated_steps"]) logger.info(f"Estimation already done (continuation): estimated_steps={estimated_steps}. Skipping pilot.") else: - # ── Run pilot in a subdirectory ─────────────────────── + # -- Run pilot in a subdirectory ----------------------- pilot_dir = os.path.join(_wd, "_pilot") os.makedirs(pilot_dir, exist_ok=True) @@ -604,7 +604,7 @@ async def _launch_auto(self, _wd): net_pilot_sec=net_pilot_sec or 0, ) - # ── Re-compute energy if post-processing parameters changed ── + # -- Re-compute energy if post-processing parameters changed -- _postproc_changed = ( estimation.get("last_num_mcmc_bin_blocks") != self.num_mcmc_bin_blocks or estimation.get("last_num_mcmc_warmup_steps") != self.num_mcmc_warmup_steps @@ -631,7 +631,7 @@ async def _launch_auto(self, _wd): ) estimation = get_estimation(_wd) - # ── Early exit if target already met ────────────────────── + # -- Early exit if target already met ---------------------- cached_energy = estimation.get("last_energy") cached_error = estimation.get("last_energy_error") if cached_energy is not None and cached_error is not None: @@ -654,9 +654,9 @@ async def _launch_auto(self, _wd): self.status = WorkflowStatus.COMPLETED return self.status, self.output_files, self.output_values - # ── Production runs (phase 1..N) ────────────────────────── + # -- Production runs (phase 1..N) -------------------------- # Three phases: - # A. Scan existing runs → find resume point + # A. Scan existing runs -> find resume point # B. Re-estimate from accumulated data (if resuming) # C. Production loop for remaining runs # @@ -670,7 +670,7 @@ async def _launch_auto(self, _wd): last_run = 0 first_new_run = self.max_continuation + 1 # assume all done - # ── Phase A: scan existing runs ── + # -- Phase A: scan existing runs -- for i in range(1, self.max_continuation + 1): recorded = get_job_by_step(_wd, i) status = recorded.get("status") @@ -682,7 +682,7 @@ async def _launch_auto(self, _wd): first_new_run = i break - # ── Phase B: re-estimate from accumulated data ── + # -- Phase B: re-estimate from accumulated data -- accumulated_measurement = 0 # measurement steps only (excl. warmup) if first_new_run > 1: cached_accum = estimation.get("accumulated_measurement_steps") @@ -725,7 +725,7 @@ async def _launch_auto(self, _wd): f"accumulated measurement: {accumulated_measurement})" ) - # ── Phase C: production loop ── + # -- Phase C: production loop -- _prev_run_steps = None for i in range(first_new_run, self.max_continuation + 1): recorded = get_job_by_step(_wd, i) @@ -797,7 +797,7 @@ async def _launch_auto(self, _wd): _prev_run_steps = estimated_steps last_run = i - # ── Side-effects: compute energy from checkpoint (if any) ── + # -- Side-effects: compute energy from checkpoint (if any) -- restart_chk = self._find_restart_chk(_wd) energy = error = None if restart_chk: @@ -821,7 +821,7 @@ async def _launch_auto(self, _wd): last_num_mcmc_warmup_steps=self.num_mcmc_warmup_steps, ) - # ── Termination decision — single source of truth ── + # -- Termination decision -- single source of truth -- vstatus, vmsg = validate_completion( _wd, self.output_values, @@ -836,7 +836,7 @@ async def _launch_auto(self, _wd): if vstatus == CompletionStatus.OK: logger.info(f" Target error achieved: {vmsg} (run {i}/{self.max_continuation})") break - # INCOMPLETE — prepare next iteration if we have an error estimate + # INCOMPLETE -- prepare next iteration if we have an error estimate if energy is not None: if i < self.max_continuation: old_steps = estimated_steps @@ -858,7 +858,7 @@ async def _launch_auto(self, _wd): f"max_continuation ({self.max_continuation}) reached" ) - # ── Final energy computation (safety net) ───────────────── + # -- Final energy computation (safety net) ----------------- last_output = step_files[last_run][1] if last_run in step_files else None restart_chk = self._find_restart_chk(_wd) if restart_chk: @@ -880,7 +880,7 @@ async def _launch_auto(self, _wd): last_num_mcmc_warmup_steps=self.num_mcmc_warmup_steps, ) - # ── Collect outputs ─────────────────────────────────────── + # -- Collect outputs --------------------------------------- chk_files = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.h5"))) output_logs = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.out"))) self.output_files = chk_files + output_logs @@ -890,7 +890,7 @@ async def _launch_auto(self, _wd): self.status = WorkflowStatus.COMPLETED return self.status, self.output_files, self.output_values - # ── Utility methods ─────────────────────────────────────────── + # -- Utility methods ------------------------------------------- def _find_restart_chk(self, work_dir: str) -> Optional[str]: """Locate the MCMC restart checkpoint file in *work_dir*.""" @@ -904,7 +904,7 @@ def _compute_energy(self, restart_chk: str, work_dir: str, output_file: Optional """Parse energy from *output_file* or run ``jqmc-tool mcmc compute-energy``. When *output_file* is given the energy is read directly from - the ``jqmc`` stdout (``Total Energy: E = … +- … Ha.``). + the ``jqmc`` stdout (``Total Energy: E = ... +- ... Ha.``). Falls back to ``jqmc-tool`` when *output_file* is *None* or when stdout parsing fails. diff --git a/jqmc_workflow/vmc_workflow.py b/jqmc_workflow/vmc_workflow.py index dc6f5470..21debb1d 100644 --- a/jqmc_workflow/vmc_workflow.py +++ b/jqmc_workflow/vmc_workflow.py @@ -1,4 +1,4 @@ -"""VMC_Workflow — Jastrow / orbital optimization via ``jqmc`` (job_type=vmc). +"""VMC_Workflow -- Jastrow / orbital optimization via ``jqmc`` (job_type=vmc). Generates a VMC input TOML, submits the ``jqmc`` binary on a remote (or local) machine, monitors until completion, and fetches the results. The optimized @@ -75,13 +75,13 @@ class VMC_Workflow(Workflow): **Automatic mode** (default, ``num_mcmc_steps=None``): - 1. **Pilot VMC run** (``_0``) — Runs a short optimisation with + 1. **Pilot VMC run** (``_0``) -- Runs a short optimisation with ``pilot_vmc_steps`` optimisation steps and ``pilot_mcmc_steps`` MCMC steps per step. The statistical error of the *last* optimisation step is used to estimate the MCMC steps per opt-step required to achieve ``target_error`` via $\sigma \propto 1/\sqrt{N}$. - 2. **Production VMC runs** (``_1``, ``_2``, …) — Full optimisation + 2. **Production VMC runs** (``_1``, ``_2``, ...) -- Full optimisation with ``num_opt_steps`` and the estimated MCMC steps per step. If a run is interrupted by the wall-time limit, the next continuation restarts from the checkpoint. At most @@ -265,7 +265,7 @@ class VMC_Workflow(Workflow): -------- MCMC_Workflow : VMC production sampling (job_type=mcmc). LRDMC_Workflow : Diffusion Monte Carlo (job_type=lrdmc-bra / lrdmc-tau). - WF_Workflow : TREXIO → hamiltonian_data conversion. + WF_Workflow : TREXIO -> hamiltonian_data conversion. """ def __init__( @@ -358,7 +358,7 @@ def __init__( # [precision] section self.precision_mode = precision_mode - # ── Input generation ────────────────────────────────────────── + # -- Input generation ------------------------------------------ def _generate_input( self, @@ -432,10 +432,10 @@ def _generate_input( filename=input_file, ) - # ── Submit / poll / fetch ───────────────────────────────────── + # -- Submit / poll / fetch ------------------------------------- # _submit_and_wait() and _make_job() are inherited from Workflow. - # ── configure / run ────────────────────────────────────────── + # -- configure / run ------------------------------------------ def configure(self) -> dict: """Validate parameters and return configuration summary.""" @@ -478,11 +478,11 @@ async def run(self) -> tuple: self._ensure_project_dir() _wd = self.project_dir - # ── Fixed-step mode ─────────────────────────────────────── + # -- Fixed-step mode --------------------------------------- if self.num_mcmc_steps is not None: return await self._launch_fixed_steps(_wd) - # ── Automatic mode (pilot + target_error) ───────────────── + # -- Automatic mode (pilot + target_error) ----------------- return await self._launch_auto(_wd) async def _launch_fixed_steps(self, _wd): @@ -554,8 +554,8 @@ async def _launch_fixed_steps(self, _wd): logger.info(f" VMC production run {i}/{self.max_continuation} completed.") - # ── Abnormal-termination guard (single source of truth) ── - # target_error=None → only Program-ends / non-finite-energy + # -- Abnormal-termination guard (single source of truth) -- + # target_error=None -> only Program-ends / non-finite-energy # checks are active. VMC's SNR/slope convergence is decided # separately at end-of-workflow. vstatus, vmsg = validate_completion(_wd, self.output_values) @@ -565,7 +565,7 @@ async def _launch_fixed_steps(self, _wd): self.status = WorkflowStatus.FAILED break - # ── Collect outputs ─────────────────────────────────────── + # -- Collect outputs --------------------------------------- h5_files = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.h5"))) output_logs = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.out"))) self.output_files = h5_files + output_logs @@ -593,7 +593,7 @@ def _h5_step_num(path: str) -> int: async def _launch_auto(self, _wd): """Automatic mode: pilot + target_error convergence.""" - # ── Phase 0: pilot estimation (skip on continuation) ────── + # -- Phase 0: pilot estimation (skip on continuation) ------ estimation = get_estimation(_wd) if estimation.get("estimated_mcmc_steps") is not None: @@ -604,7 +604,7 @@ async def _launch_auto(self, _wd): "Skipping pilot." ) else: - # ── Run pilot in a subdirectory ─────────────────────── + # -- Run pilot in a subdirectory ----------------------- pilot_dir = os.path.join(_wd, "_pilot") os.makedirs(pilot_dir, exist_ok=True) @@ -721,7 +721,7 @@ async def _launch_auto(self, _wd): net_pilot_sec=net_pilot_sec or 0, ) - # ── Production runs (phase 1..N) ────────────────────────── + # -- Production runs (phase 1..N) -------------------------- _has_convergence_criteria = self.target_snr is not None or self.energy_slope_sigma_threshold is not None last_run = 0 step_files = {} # {step: (input, output, run_id)} @@ -742,7 +742,7 @@ async def _launch_auto(self, _wd): run_id_i = recorded.get("run_id", "") logger.info(f" step {i}: already {status}. Resuming...") else: - # ── Re-evaluate convergence from fetched runs ───── + # -- Re-evaluate convergence from fetched runs ----- if _has_convergence_criteria and last_run > 0 and not _checked_fetched_convergence: _checked_fetched_convergence = True converged, converged_snr, converged_slope = self._check_convergence( @@ -814,8 +814,8 @@ async def _launch_auto(self, _wd): logger.info(f" VMC production run {i}/{self.max_continuation} completed.") - # ── Abnormal-termination guard (single source of truth) ── - # target_error=None → only Program-ends / non-finite-energy + # -- Abnormal-termination guard (single source of truth) -- + # target_error=None -> only Program-ends / non-finite-energy # checks; SNR/slope convergence is evaluated separately below. vstatus, vmsg = validate_completion(_wd, self.output_values) if vstatus == CompletionStatus.FAILED: @@ -824,7 +824,7 @@ async def _launch_auto(self, _wd): self.status = WorkflowStatus.FAILED break - # ── Early exit if convergence criteria met ──────────── + # -- Early exit if convergence criteria met ------------ if _has_convergence_criteria and i < self.max_continuation: converged, converged_snr, converged_slope = self._check_convergence( _wd, @@ -835,7 +835,7 @@ async def _launch_auto(self, _wd): logger.info(f" Convergence achieved at run {i}/{self.max_continuation}. Stopping early.") break - # ── Collect outputs ─────────────────────────────────────── + # -- Collect outputs --------------------------------------- h5_files = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.h5"))) output_logs = sorted(os.path.basename(f) for f in glob.glob(os.path.join(_wd, "*.out"))) self.output_files = h5_files + output_logs @@ -857,7 +857,7 @@ def _h5_step_num(path: str) -> int: last_output = os.path.join(_wd, step_files[last_run][1]) self._parse_output(last_output) - # ── Final convergence check ─────────────────────────────── + # -- Final convergence check ------------------------------- if converged is None: converged, converged_snr, converged_slope = self._check_convergence( _wd, @@ -884,7 +884,7 @@ def _h5_step_num(path: str) -> int: self.status = WorkflowStatus.COMPLETED return self.status, self.output_files, self.output_values - # ── Utility methods ─────────────────────────────────────────── + # -- Utility methods ------------------------------------------- def _check_convergence( self, @@ -900,7 +900,7 @@ def _check_convergence( converged_snr = True converged_slope = True - # ── (A) SNR check ── + # -- (A) SNR check -- if self.target_snr is not None: all_snr = [] for j in range(last_run, 0, -1): @@ -921,7 +921,7 @@ def _check_convergence( converged_snr = False logger.warning(" Could not parse S/N from production output.") - # ── (B) Energy slope check ── + # -- (B) Energy slope check -- if self.energy_slope_sigma_threshold is not None: all_energies: list[tuple[float, float]] = [] for j in range(last_run, 0, -1): @@ -966,7 +966,7 @@ def _find_restart_chk(self, work_dir: str) -> Optional[str]: return os.path.basename(matches[-1]) return None - # ── Output parsing ──────────────────────────────────────────── + # -- Output parsing -------------------------------------------- def _parse_output(self, output_file=None): """Extract the last optimization energy from *output_file*.""" @@ -1027,7 +1027,7 @@ def _parse_all_energies(output_file: str) -> list[tuple[float, float]]: Returns ------- list[tuple[float, float]] - ``[(E_1, σ_1), (E_2, σ_2), ...]`` in file order. + ``[(E_1, sigma_1), (E_2, sigma_2), ...]`` in file order. Empty list if the file is missing or unparseable. """ if not os.path.isfile(output_file): @@ -1049,7 +1049,7 @@ def _fit_energy_slope( ) -> tuple[float, float]: """Weighted linear regression of energy vs optimisation step. - Model: ``E_k = a + b * k + ε_k``, weight ``w_k = 1 / σ_k²``. + Model: ``E_k = a + b * k + eps_k``, weight ``w_k = 1 / sigma_k^2``. Parameters ---------- diff --git a/jqmc_workflow/wf_workflow.py b/jqmc_workflow/wf_workflow.py index e84d568c..6a5cd25b 100644 --- a/jqmc_workflow/wf_workflow.py +++ b/jqmc_workflow/wf_workflow.py @@ -1,4 +1,4 @@ -"""WF_Workflow — TREXIO to hamiltonian_data.h5 conversion. +"""WF_Workflow -- TREXIO to hamiltonian_data.h5 conversion. Wraps ``jqmc-tool trexio convert-to`` which converts a TREXIO file (.h5) into the internal ``hamiltonian_data.h5`` format, optionally attaching @@ -82,9 +82,9 @@ class WF_Workflow(Workflow): Extra NN Jastrow parameters (``-jp key=value``). ao_conv_to : str, optional Convert AOs after building the Hamiltonian (``--ao-conv-to``). - ``"cart"`` → convert to Cartesian AOs, - ``"sphe"`` → convert to spherical-harmonic AOs, - ``None`` → keep the original representation. + ``"cart"`` -> convert to Cartesian AOs, + ``"sphe"`` -> convert to spherical-harmonic AOs, + ``None`` -> keep the original representation. Example ------- @@ -100,7 +100,7 @@ class WF_Workflow(Workflow): Notes ----- - This workflow runs **locally** — no remote job submission is + This workflow runs **locally** -- no remote job submission is involved. It calls ``jqmc-tool trexio convert-to`` via :func:`subprocess.run`. @@ -174,7 +174,7 @@ def configure(self) -> dict: } async def run(self) -> tuple: - """Run the TREXIO→hamiltonian conversion (locally). + """Run the TREXIO->hamiltonian conversion (locally). Returns ------- diff --git a/jqmc_workflow/workflow.py b/jqmc_workflow/workflow.py index 48eaae31..70099e19 100644 --- a/jqmc_workflow/workflow.py +++ b/jqmc_workflow/workflow.py @@ -68,9 +68,9 @@ logger = getLogger("jqmc-workflow").getChild(__name__) -# ═══════════════════════════════════════════════════════════════════ +# =================================================================== # Dependency specification helpers -# ═══════════════════════════════════════════════════════════════════ +# =================================================================== class FileFrom: @@ -146,14 +146,14 @@ class ValueFrom: See the *Output Values* section of each workflow class for available keys: - * :class:`VMC_Workflow` — ``optimized_hamiltonian``, - ``energy``, ``energy_error``, ``checkpoint``, … - * :class:`MCMC_Workflow` — ``energy``, ``energy_error``, - ``restart_chk``, ``forces``, … - * :class:`LRDMC_Workflow` — ``energy``, ``energy_error``, - ``alat``, ``restart_chk``, ``forces``, … - * :class:`LRDMC_Ext_Workflow` — ``extrapolated_energy``, - ``extrapolated_energy_error``, ``per_alat_results``, … + * :class:`VMC_Workflow` -- ``optimized_hamiltonian``, + ``energy``, ``energy_error``, ``checkpoint``, ... + * :class:`MCMC_Workflow` -- ``energy``, ``energy_error``, + ``restart_chk``, ``forces``, ... + * :class:`LRDMC_Workflow` -- ``energy``, ``energy_error``, + ``alat``, ``restart_chk``, ``forces``, ... + * :class:`LRDMC_Ext_Workflow` -- ``extrapolated_energy``, + ``extrapolated_energy_error``, ``per_alat_results``, ... Examples -------- @@ -187,15 +187,15 @@ def _is_dependency(obj) -> bool: return isinstance(obj, (FileFrom, ValueFrom)) -# ═══════════════════════════════════════════════════════════════════ +# =================================================================== # Base Workflow -# ═══════════════════════════════════════════════════════════════════ +# =================================================================== class Workflow: """Abstract base class for all jQMC computation workflows. - Every concrete workflow (VMC, MCMC, LRDMC, WF, …) inherits from + Every concrete workflow (VMC, MCMC, LRDMC, WF, ...) inherits from this class and overrides :meth:`configure` and :meth:`run`. Parameters @@ -211,7 +211,7 @@ class Workflow: successfully (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). Local files matching the patterns are always removed. Remote files are removed only when the workflow targets a remote - machine. Default is *None* (empty list — no cleanup). + machine. Default is *None* (empty list -- no cleanup). Attributes ---------- @@ -222,7 +222,7 @@ class Workflow: output_files : list[str] Filenames produced by the workflow (populated after run). output_values : dict - Scalar results (energy, error, …) produced by the workflow. + Scalar results (energy, error, ...) produced by the workflow. project_dir : str or None Working directory for file I/O. Resolved to an absolute path. cleanup_patterns : list[str] @@ -259,7 +259,7 @@ def __init__(self, project_dir: Optional[str] = None, cleanup_patterns: Optional self._bg_task: Optional[asyncio.Task] = None self.cleanup_patterns: List[str] = cleanup_patterns or [] - # ── Filename generation (per-job run_id) ────────────────────── + # -- Filename generation (per-job run_id) ---------------------- @staticmethod def _new_run_id() -> str: @@ -298,7 +298,7 @@ def _cleanup_files(self): server_machine_name = getattr(self, "server_machine_name", None) if server_machine_name is None: - # No remote machine — local-only cleanup + # No remote machine -- local-only cleanup import glob as _glob for pattern in self.cleanup_patterns: @@ -318,7 +318,7 @@ def _cleanup_files(self): raise dt.ssh_close() - # ── configure / run (new primary interface) ───────────────────── + # -- configure / run (new primary interface) --------------------- def configure(self) -> dict: """Validate parameters and generate inputs (no execution). @@ -328,7 +328,7 @@ def configure(self) -> dict: return {} async def run(self) -> tuple: - """Execute the workflow (submit → poll → fetch → convergence loop). + """Execute the workflow (submit -> poll -> fetch -> convergence loop). Override in subclass. Must return ``(status, output_files, output_values)``. @@ -339,7 +339,7 @@ async def run(self) -> tuple: self._ensure_project_dir() return self.status, self.output_files, self.output_values - # ── Full lifecycle (backward-compatible) ────────────────────── + # -- Full lifecycle (backward-compatible) ---------------------- async def async_launch(self): """Run configure() + run(). Backward-compatible entry point.""" @@ -350,9 +350,9 @@ async def async_launch(self): def launch(self): return asyncio.run(self.async_launch()) - # ── Phased execution (MCP interactive mode) ─────────────────── + # -- Phased execution (MCP interactive mode) ------------------- # - # Used by MCP tools: submit(action) → poll() → collect(). + # Used by MCP tools: submit(action) -> poll() -> collect(). # submit() starts run() as a background asyncio.Task. # # Usage pattern:: @@ -438,7 +438,7 @@ async def async_collect(self) -> dict: **output_values, } - # ── Common job helpers (used by VMC / MCMC / LRDMC) ─────────── + # -- Common job helpers (used by VMC / MCMC / LRDMC) ----------- # # These methods require the following attributes on *self*: # server_machine_name, hamiltonian_file, queue_label, @@ -475,15 +475,15 @@ async def _submit_and_wait( ): """Submit a job, poll until done, fetch results. - This method is CWD-safe — it never calls ``os.chdir()``. + This method is CWD-safe -- it never calls ``os.chdir()``. All path context is passed explicitly via *work_dir*. Restart behaviour is driven by ``workflow_state.toml``: - * ``fetched`` — skip entirely (already done) - * ``completed`` — fetch results only - * ``submitted`` — resume waiting, then fetch - * no record — submit a new job + * ``fetched`` -- skip entirely (already done) + * ``completed`` -- fetch results only + * ``submitted`` -- resume waiting, then fetch + * no record -- submit a new job Parameters ---------- @@ -523,7 +523,7 @@ async def _submit_and_wait( finally: job_tmp._close_ssh() - # ── Restart detection via job history ───────────────────── + # -- Restart detection via job history --------------------- if step is not None: recorded = get_job_by_step(work_dir, step) else: @@ -565,7 +565,7 @@ async def _submit_and_wait( update_job(work_dir, input_file, status="fetched", fetched_at=_now_iso()) return - # ── New submission ──────────────────────────────────────── + # -- New submission ---------------------------------------- job = self._make_job(input_file, output_file, queue_label=queue_label, run_id=run_id) try: submit_sh = self._submit_script_name(run_id) @@ -624,9 +624,9 @@ def _make_job(self, input_file, output_file, queue_label=None, run_id=""): ) -# ═══════════════════════════════════════════════════════════════════ +# =================================================================== # Container -# ═══════════════════════════════════════════════════════════════════ +# =================================================================== class Container: @@ -635,13 +635,13 @@ class Container: ``Container`` is the standard wrapper used with the :class:`Launcher`. It manages: - * **Directory creation** — a self-contained project directory is + * **Directory creation** -- a self-contained project directory is created under the current working directory. - * **Input file copying** — source files (or resolved + * **Input file copying** -- source files (or resolved :class:`FileFrom` references) are copied into the project dir. - * **State tracking** — a ``workflow_state.toml`` file records - lifecycle status (``pending`` → ``running`` → ``completed``). - * **Re-entrance** — if the directory already exists with status + * **State tracking** -- a ``workflow_state.toml`` file records + lifecycle status (``pending`` -> ``running`` -> ``completed``). + * **Re-entrance** -- if the directory already exists with status ``completed``, the workflow is *not* re-run; outputs are read from the state file instead. @@ -718,7 +718,7 @@ def __init__( self.root_dir = os.getcwd() self.project_dir = os.path.join(self.root_dir, self.dirname) - # ── Preparation ─────────────────────────────────────────────── + # -- Preparation ----------------------------------------------- def _prepare(self): """Create project dir, copy input files, write initial state.""" @@ -788,7 +788,7 @@ def _ensure_input_files(self): """Copy any missing input files into an existing project directory. Unlike :meth:`_copy_input_files`, this does *not* overwrite files - that already exist in the project directory — it only fills in + that already exist in the project directory -- it only fills in the gaps (e.g. after a failed first run that created the directory but never completed the copy). """ @@ -864,22 +864,22 @@ def _check_input_staleness(self, proj: str) -> bool: """ recorded = get_input_fingerprints(proj) if not recorded: - return False # no fingerprints recorded — cannot check + return False # no fingerprints recorded -- cannot check current = self._compute_input_fingerprints() for name, cur_fp in current.items(): rec_fp = recorded.get(name) if rec_fp is None: - # New input file not in original — treat as stale + # New input file not in original -- treat as stale return True if cur_fp.get("sha256") != rec_fp.get("sha256"): logger.warning( f"[{self.label}] Input '{name}' has changed since last run " - f"(sha256: {rec_fp.get('sha256', '?')[:12]}… → {cur_fp.get('sha256', '?')[:12]}…)." + f"(sha256: {rec_fp.get('sha256', '?')[:12]}... -> {cur_fp.get('sha256', '?')[:12]}...)." ) return True return False - # ── Launch ──────────────────────────────────────────────────── + # -- Launch ---------------------------------------------------- async def async_launch(self): proj = os.path.abspath(self.project_dir) @@ -911,7 +911,7 @@ async def async_launch(self): # Validate required files before running. self._validate_input_files(proj) - # Run the workflow — pass project_dir explicitly instead of + # Run the workflow -- pass project_dir explicitly instead of # relying on os.chdir(). update_status(proj, WorkflowStatus.RUNNING) self.workflow.project_dir = proj @@ -923,7 +923,7 @@ async def async_launch(self): update_status(proj, WorkflowStatus.FAILED) raise - # Write completion — but only if the workflow did not fail. + # Write completion -- but only if the workflow did not fail. if self.status != WorkflowStatus.FAILED: # Run all post-completion validation checks in one place. # Post-hoc mode (target_error=None): only OK / FAILED are possible. @@ -933,7 +933,7 @@ async def async_launch(self): for k, v in self.output_values.items(): result_fields[f"result_{k}"] = v update_status(proj, WorkflowStatus.COMPLETED, **result_fields) - # ── Post-completion cleanup ── + # -- Post-completion cleanup -- try: self.workflow._cleanup_files() except Exception as e: @@ -966,7 +966,7 @@ def _collect_outputs(self): def launch(self): return asyncio.run(self.async_launch()) - # ── Phased execution (delegates to inner Workflow) ──────────── + # -- Phased execution (delegates to inner Workflow) ------------ async def async_submit(self, action: str = "run") -> dict: """Start the container's workflow in the background. diff --git a/prototypes/block_diagonal_matrix.py b/prototypes/block_diagonal_matrix.py index b6cbff9b..f26ae612 100644 --- a/prototypes/block_diagonal_matrix.py +++ b/prototypes/block_diagonal_matrix.py @@ -17,11 +17,11 @@ def compute_sparse_mul_jnp(A, x): return jnp.dot(A, x) -# 例として、3つのブロック正方行列を持つ場合 +# 例として,3つのブロック正方行列を持つ場合 num_block = 3 size_block_list = [2, 3, 4] -# 空のリストを用意して、疎行列のブロックを格納 +# 空のリストを用意して,疎行列のブロックを格納 blocks = [] for i in range(num_block): @@ -39,7 +39,7 @@ def compute_sparse_mul_jnp(A, x): # scipy.sparse.bmatを使って疎ブロック行列を作成 sparse_block_matrix = bmat(blocks, format="csr") -# ベクトルを作成(疎行列の列数に合わせたベクトルを生成) +# ベクトルを作成(疎行列の列数に合わせたベクトルを生成) vector = np.random.rand(sparse_block_matrix.shape[1]) print(sparse_block_matrix) diff --git a/prototypes/vmc_vmap.py b/prototypes/vmc_vmap.py index 23142773..84299d68 100644 --- a/prototypes/vmc_vmap.py +++ b/prototypes/vmc_vmap.py @@ -113,12 +113,12 @@ def run_mcmc(latest_r_up_carts, latest_r_dn_carts, key, A, B): # total number of electrons total_electrons = len(latest_r_up_carts) + len(latest_r_dn_carts) - # 0〜total_electrons-1からランダムに選択 + # 0~total_electrons-1からランダムに選択 key, subkey = jax.random.split(key) rand_num = jax.random.randint(subkey, shape=(), minval=0, maxval=total_electrons) # "up"か"dn"を判定するためのブーリアン値 - # is_up == Trueならup、Falseならdn + # is_up == Trueならup,Falseならdn is_up = rand_num < len(latest_r_up_carts) # upから選ぶ電子index @@ -137,7 +137,7 @@ def run_mcmc(latest_r_up_carts, latest_r_dn_carts, key, A, B): is_up, latest_r_up_carts[selected_electron_index], latest_r_dn_carts[selected_electron_index] ) - # スピンを数値(0: up, 1: dn)で持つ + # スピンを数値(0: up, 1: dn)で持つ selected_electron_spin = jnp.where(is_up, 0, 1) sigma = 1.0 @@ -146,7 +146,7 @@ def run_mcmc(latest_r_up_carts, latest_r_dn_carts, key, A, B): key, subkey = jax.random.split(key) g = jax.random.normal(subkey, shape=()) * sigma - # 0〜2の範囲でランダムなインデックスを選択 + # 0~2の範囲でランダムなインデックスを選択 key, subkey = jax.random.split(key) random_index = jax.random.randint(subkey, shape=(), minval=0, maxval=3) diff --git a/pyproject.toml b/pyproject.toml index 5724157d..9f3f7100 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,14 +10,70 @@ local_scheme = "no-local-version" [tool.ruff] line-length = 128 lint.select = [ - "F", # Flake8 - "B", # Black - "I", # isort - "E", # pycodestyle-errors - "D", # pydocstyle + "F", # pyflakes + "E", "W", # pycodestyle errors + warnings + "B", # flake8-bugbear + "I", # isort + "D", # pydocstyle + "UP", # pyupgrade + "C4", # comprehensions + "PIE", # idiom + "SIM", # simplify + "RET", # return + "G", # logging-format + "T20", # print + "NPY", # numpy + "PT", # pytest-style + "PERF", # performance + "RUF", # ruff-specific (includes RUF001-003) ] -lint.extend-ignore = ["I001", "B008", "B023", "D417", "D100", "E501", "E741"] -exclude = [] +lint.extend-ignore = [ + # --- Pre-existing intentional ignores --- + "I001", "B008", "B023", "D417", "D100", "E501", "E741", + "G004", # f-string in logger (jQMC convention) + "RUF012", # mutable class default (flax struct.dataclass false positive) + "RUF013", # implicit Optional (`X = None` then `if X is None` is OK) + "PERF203", # try-except in loop (sometimes required) + "PT011", # pytest.raises too broad + "SIM108", # ternary if-else (sometimes if-block is more readable) + # --- TODO: currently-violating rules; fix and remove from this list --- + # bugbear + "B007", "B010", "B904", "B905", + # flake8-comprehensions + "C401", "C408", "C409", "C414", "C416", "C419", "C420", + # pydocstyle + "D101", "D102", "D103", "D104", "D105", "D107", "D200", "D202", + "D205", "D209", "D210", "D212", "D301", "D403", "D415", "D416", + # pycodestyle errors + "E101", "E401", "E402", "E701", "E702", "E721", "E902", + # pyflakes + "F401", "F541", "F811", "F841", + # logging-format + "G003", + # numpy + "NPY002", + # performance + "PERF401", "PERF402", + # flake8-pie + "PIE790", "PIE808", + # flake8-pytest-style + "PT006", "PT012", "PT018", + # flake8-return + "RET501", "RET503", "RET504", "RET505", "RET506", "RET507", "RET508", + # ruff-specific (RUF001/002/003 are ENFORCED — never add here) + "RUF005", "RUF010", "RUF019", "RUF021", "RUF022", "RUF023", + "RUF046", "RUF059", "RUF100", + # flake8-simplify + "SIM101", "SIM102", "SIM103", "SIM105", "SIM114", "SIM115", "SIM118", "SIM300", + # flake8-print + "T201", + # pyupgrade + "UP006", "UP007", "UP009", "UP015", "UP022", "UP024", "UP031", + "UP032", "UP033", "UP034", "UP035", "UP037", "UP045", + # pycodestyle warnings + "W291", "W293", "W605", +] +exclude = ["jqmc/obsolete", "prototypes"] [tool.ruff.lint.pydocstyle] convention = "google" diff --git a/scripts/check_ascii.sh b/scripts/check_ascii.sh new file mode 100755 index 00000000..bbbe9689 --- /dev/null +++ b/scripts/check_ascii.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Reject non-ASCII bytes in Python sources. +# +# Allowed: printable ASCII (0x20..0x7E), TAB (0x09), LF (0x0A), CR (0x0D). +# Everything else is rejected. + +set -e +exec python3 - "$@" <<'PYEOF' +import sys + +ALLOWED = set(range(0x20, 0x7F)) | {0x09, 0x0A, 0x0D} + +rc = 0 +for path in sys.argv[1:]: + with open(path, "rb") as f: + data = f.read() + bad_lines = {} + line_no = 1 + for b in data: + if b == 0x0A: + line_no += 1 + elif b not in ALLOWED: + bad_lines.setdefault(line_no, set()).add(b) + if bad_lines: + text = data.decode("utf-8", errors="replace").splitlines() + for n in sorted(bad_lines): + content = text[n - 1] if n - 1 < len(text) else "" + byte_list = ", ".join(f"0x{b:02X}" for b in sorted(bad_lines[n])) + print(f"{path}:{n}: bytes [{byte_list}]: {content}") + print(f"ERROR: non-ASCII detected in {path}") + rc = 1 +sys.exit(rc) +PYEOF diff --git a/tests/test_AOs.py b/tests/test_AOs.py index b42b1f4e..0a3ee74a 100755 --- a/tests/test_AOs.py +++ b/tests/test_AOs.py @@ -1253,7 +1253,7 @@ def test_fused_AOs_value_grad_lap_matches_split(trexio_file: str): # value: tight ao_eval-zone tolerance. Fused reuses the grad/lap # ``phi`` (left-to-right multiplication chain), while standalone - # ``compute_AOs`` parenthesises the polynomial separately — strictly + # ``compute_AOs`` parenthesises the polynomial separately -- strictly # ULP-level differences are allowed. atol_val, rtol_val = get_tolerance("ao_eval", "strict") assert_allclose(np.asarray(val_f), np.asarray(val_s), atol=atol_val, rtol=rtol_val) @@ -1271,7 +1271,7 @@ def test_fused_AOs_value_grad_lap_matches_split(trexio_file: str): def test_fused_AOs_dtypes_match_zones(trexio_file: str): """``compute_AOs_value_grad_lap`` outputs are pinned to their zones. - val ↔ ``ao_eval`` (fp32 mixed / fp64 full); gx/gy/gz/lap ↔ + val <-> ``ao_eval`` (fp32 mixed / fp64 full); gx/gy/gz/lap <-> ``ao_grad_lap`` (fp64 always). """ ( diff --git a/tests/test_MOs.py b/tests/test_MOs.py index 74fbe50b..c56f00c8 100755 --- a/tests/test_MOs.py +++ b/tests/test_MOs.py @@ -643,7 +643,7 @@ def test_MOs_cart_to_sphe(): ao_idx = 0 for l in range(7): # l = 0..6 shell_indices: list[int] = [] - for nx, ny, nz in [(nx, ny, l - nx - ny) for nx in range(l, -1, -1) for ny in range(l - nx, -1, -1)]: + for nx, ny, nz in [(_nx, _ny, l - _nx - _ny) for _nx in range(l, -1, -1) for _ny in range(l - _nx, -1, -1)]: nucleus_index.append(0) orbital_indices.append(ao_idx) exponents.append(float(l + 1)) @@ -789,8 +789,8 @@ def test_fused_MOs_value_grad_lap_matches_split(trexio_file: str): def test_fused_MOs_dtypes_match_zones(trexio_file: str): """``compute_MOs_value_grad_lap`` outputs are pinned to their zones. - val ↔ ``mo_eval`` (fp32 mixed / fp64 full); gx/gy/gz ↔ ``mo_grad`` - (fp64); lap ↔ ``mo_lap`` (fp64). + val <-> ``mo_eval`` (fp32 mixed / fp64 full); gx/gy/gz <-> ``mo_grad`` + (fp64); lap <-> ``mo_lap`` (fp64). """ parsed = read_trexio_file( trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), diff --git a/tests/test_ao_basis_optimization.py b/tests/test_ao_basis_optimization.py index d6ab1465..d106b226 100644 --- a/tests/test_ao_basis_optimization.py +++ b/tests/test_ao_basis_optimization.py @@ -755,6 +755,6 @@ def test_opt_with_projected_MOs_lambda_basis_conflict(): # J3_basis_exp=True should NOT conflict with opt_with_projected_MOs flags_j3 = [True, False, False, False] # opt_J3_basis_exp=True - # This should not raise — J3 basis does not affect MO projection overlap + # This should not raise -- J3 basis does not affect MO projection overlap if opt_with_projected_MOs and any(flags_j3[2:]): raise ValueError("Should not reach here") diff --git a/tests/test_checkpoint_components.py b/tests/test_checkpoint_components.py index 9d065989..cd020053 100644 --- a/tests/test_checkpoint_components.py +++ b/tests/test_checkpoint_components.py @@ -1,4 +1,4 @@ -"""Tests for jqmc.checkpoint — Phase 0 infrastructure.""" +"""Tests for jqmc.checkpoint -- Phase 0 infrastructure.""" import os import sys @@ -158,7 +158,7 @@ def _make_sample_data(): ) def test_save_load_roundtrip(self, tmp_path): - """Data survives save → merge → load.""" + """Data survives save -> merge -> load.""" data = self._make_sample_data() rank_file = str(tmp_path / "._restart_rank0.h5") save_rank_checkpoint(rank_file, **data) @@ -169,7 +169,7 @@ def test_save_load_roundtrip(self, tmp_path): # Now merge into a single checkpoint (simulate rank 0) from jqmc._checkpoint import merge_rank_checkpoints - # We need a Hamiltonian_data for merging — use a minimal mock + # We need a Hamiltonian_data for merging -- use a minimal mock merged = str(tmp_path / "restart.h5") _merge_single_rank(rank_file, merged, tmp_path) @@ -357,7 +357,7 @@ def test_none(self): # --------------------------------------------------------------------------- -# Test utilities — minimal merge helpers that don't require Hamiltonian_data +# Test utilities -- minimal merge helpers that don't require Hamiltonian_data # --------------------------------------------------------------------------- diff --git a/tests/test_checkpoint_gfmc.py b/tests/test_checkpoint_gfmc.py index 2528b57e..aefdfe34 100644 --- a/tests/test_checkpoint_gfmc.py +++ b/tests/test_checkpoint_gfmc.py @@ -1,4 +1,4 @@ -"""Tests for GFMC_t / GFMC_n save_to_hdf5 / load_from_hdf5 — Phase 2.""" +"""Tests for GFMC_t / GFMC_n save_to_hdf5 / load_from_hdf5 -- Phase 2.""" import os import sys @@ -173,7 +173,7 @@ def _save_merge_gfmc_n(gfmc, hd, tmp_path, mpi_size=1, rank=0): @pytest.mark.parametrize("trexio_file", TREXIO_FILES, ids=lambda f: f.replace(".h5", "")) @pytest.mark.parametrize("jastrow_combo", JASTROW_COMBOS) class TestGFMCtSaveLoadRoundtrip: - """Round-trip tests: GFMC_t → save → merge → load → compare.""" + """Round-trip tests: GFMC_t -> save -> merge -> load -> compare.""" @pytest.fixture(autouse=True) def _setup(self, trexio_file, jastrow_combo, tmp_path): @@ -337,7 +337,7 @@ def test_force_derivative_roundtrip(self): @pytest.mark.parametrize("trexio_file", TREXIO_FILES, ids=lambda f: f.replace(".h5", "")) @pytest.mark.parametrize("jastrow_combo", JASTROW_COMBOS) class TestGFMCnSaveLoadRoundtrip: - """Round-trip tests: GFMC_n → save → merge → load → compare.""" + """Round-trip tests: GFMC_n -> save -> merge -> load -> compare.""" @pytest.fixture(autouse=True) def _setup(self, trexio_file, jastrow_combo, tmp_path): diff --git a/tests/test_checkpoint_mcmc.py b/tests/test_checkpoint_mcmc.py index eaa790ab..da868377 100644 --- a/tests/test_checkpoint_mcmc.py +++ b/tests/test_checkpoint_mcmc.py @@ -1,4 +1,4 @@ -"""Tests for MCMC.save_to_hdf5 / MCMC.load_from_hdf5 — Phase 1.""" +"""Tests for MCMC.save_to_hdf5 / MCMC.load_from_hdf5 -- Phase 1.""" import os import sys @@ -145,14 +145,14 @@ def _save_merge(mcmc, hd, tmp_path, mpi_size=1, rank=0): # --------------------------------------------------------------------------- -# Tests — basic round-trip (parameterized by trexio file × Jastrow combo) +# Tests -- basic round-trip (parameterized by trexio file x Jastrow combo) # --------------------------------------------------------------------------- @pytest.mark.parametrize("trexio_file", TREXIO_FILES, ids=lambda f: f.replace(".h5", "")) @pytest.mark.parametrize("jastrow_combo", JASTROW_COMBOS) class TestMCMCSaveLoadRoundtrip: - """Round-trip tests: MCMC → save → merge → load → compare.""" + """Round-trip tests: MCMC -> save -> merge -> load -> compare.""" @pytest.fixture(autouse=True) def _setup(self, trexio_file, jastrow_combo, tmp_path): @@ -294,7 +294,7 @@ def test_force_derivative_roundtrip(self): # --------------------------------------------------------------------------- -# Tests — optax optimizer round-trip (only combos that have variational params) +# Tests -- optax optimizer round-trip (only combos that have variational params) # --------------------------------------------------------------------------- # Jastrow combos that have variational parameters (needed for run_optimize) @@ -323,7 +323,7 @@ def _setup(self, trexio_file, jastrow_combo, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) def test_optax_adam_state_roundtrip(self): - """After 1 optax optimization step, optimizer_runtime survives save→load.""" + """After 1 optax optimization step, optimizer_runtime survives save->load.""" import jax.tree_util as tu from jqmc.jqmc_mcmc import MCMC diff --git a/tests/test_determinant.py b/tests/test_determinant.py index fe2cae9e..2ddd11eb 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -310,7 +310,7 @@ def _build_sphe_aos_l_le6(rng: np.random.Generator) -> AOs_sphe_data: def test_geminal_sphe_to_cart_AOs_data(): - """Round-trip AOs l<=6: spherical→Cartesian keeps geminal values/grads.""" + """Round-trip AOs l<=6: spherical->Cartesian keeps geminal values/grads.""" # Comparison crosses ao_eval/det_eval (values) and ao_grad_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad_lap", "det_grad_lap"), "strict") @@ -350,7 +350,7 @@ def test_geminal_sphe_to_cart_AOs_data(): def test_geminal_cart_to_sphe_AOs_data(): - """Round-trip AOs l<=6: Cartesian→spherical keeps geminal values/grads.""" + """Round-trip AOs l<=6: Cartesian->spherical keeps geminal values/grads.""" # Comparison crosses ao_eval/det_eval (values) and ao_grad_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. atol_c, rtol_c = get_tolerance_min(("ao_eval", "det_eval", "ao_grad_lap", "det_grad_lap"), "strict") @@ -392,7 +392,7 @@ def test_geminal_cart_to_sphe_AOs_data(): def test_geminal_sphe_to_cart_MOs_data(): - """Round-trip MOs built on l<=6 AOs: spherical→Cartesian keeps geminal values/grads.""" + """Round-trip MOs built on l<=6 AOs: spherical->Cartesian keeps geminal values/grads.""" # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad_lap/mo_grad/mo_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. atol_c, rtol_c = get_tolerance_min( @@ -439,7 +439,7 @@ def test_geminal_sphe_to_cart_MOs_data(): def test_geminal_cart_to_sphe_MOs_data(): - """Round-trip MOs l<=6: Cartesian→spherical keeps geminal values/grads.""" + """Round-trip MOs l<=6: Cartesian->spherical keeps geminal values/grads.""" # Comparison crosses ao_eval/mo_eval/det_eval (values) and ao_grad_lap/mo_grad/mo_lap/det_grad_lap (grads); # achievable agreement is bounded by the loosest zone on the path. atol_c, rtol_c = get_tolerance_min( @@ -822,7 +822,7 @@ def test_grads_and_laplacian_fast_update(trexio_file: str): r_dn_carts=r_dn_carts, ) - # Debug helper above is autodiff through compute_ln_det → bottlenecked by + # Debug helper above is autodiff through compute_ln_det -> bottlenecked by # ao_eval (fp32 in mixed mode); fast path is fp64 (ao_grad_lap), so the # achievable agreement is bounded by ao_eval, not det_grad_lap. atol, rtol = get_tolerance_min(["ao_eval", "det_grad_lap"], "strict") @@ -1267,7 +1267,7 @@ def test_analytic_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): r_dn_carts=r_dn_carts, ) - # Auto path is autodiff through compute_ln_det → bottlenecked by ao_eval + # Auto path is autodiff through compute_ln_det -> bottlenecked by ao_eval # (fp32 in mixed mode); analytic path is fp64 (ao_grad_lap), so achievable # agreement is bounded by ao_eval, not det_grad_lap. atol, rtol = get_tolerance_min(["ao_eval", "det_grad_lap"], "strict") @@ -1501,7 +1501,7 @@ def _make_geminal_with_lambda(lam, num_up=1, num_dn=1): def test_symmetrize_lambda_square_symmetric(): - """L1-1: square symmetric lambda → 0.5*(mat+mat.T).""" + """L1-1: square symmetric lambda -> 0.5*(mat+mat.T).""" rng = np.random.RandomState(0) n = 5 lam_sym = rng.randn(n, n) @@ -1516,7 +1516,7 @@ def test_symmetrize_lambda_square_symmetric(): def test_symmetrize_lambda_square_nonsymmetric(): - """L1-2: square non-symmetric lambda → no-op.""" + """L1-2: square non-symmetric lambda -> no-op.""" rng = np.random.RandomState(1) n = 5 lam_nonsym = rng.randn(n, n) @@ -1529,7 +1529,7 @@ def test_symmetrize_lambda_square_nonsymmetric(): def test_symmetrize_lambda_rect_paired_symmetric(): - """L1-3: rectangular lambda, paired sub-block symmetric → only paired part symmetrized.""" + """L1-3: rectangular lambda, paired sub-block symmetric -> only paired part symmetrized.""" rng = np.random.RandomState(2) n_up = 3 n_extra = 2 @@ -1549,7 +1549,7 @@ def test_symmetrize_lambda_rect_paired_symmetric(): def test_symmetrize_lambda_rect_paired_nonsymmetric(): - """L1-4: rectangular lambda, paired sub-block non-symmetric → no-op.""" + """L1-4: rectangular lambda, paired sub-block non-symmetric -> no-op.""" rng = np.random.RandomState(3) n_up = 3 n_extra = 2 @@ -1565,7 +1565,7 @@ def test_symmetrize_lambda_rect_paired_nonsymmetric(): def test_symmetrize_lambda_none(): - """L1-5: lambda_matrix=None → no-op.""" + """L1-5: lambda_matrix=None -> no-op.""" gd_none = Geminal_data(lambda_matrix=None) mat = np.random.RandomState(4).randn(3, 3) result = gd_none.symmetrize_lambda(mat) @@ -1573,7 +1573,7 @@ def test_symmetrize_lambda_none(): def test_symmetrize_lambda_1d(): - """L1-6: 1-D lambda (ndim!=2) → no-op.""" + """L1-6: 1-D lambda (ndim!=2) -> no-op.""" gd = _make_geminal_with_lambda(np.array([1.0, 2.0, 3.0])) mat = np.random.RandomState(5).randn(3, 3) result = gd.symmetrize_lambda(mat) diff --git a/tests/test_hamiltonian.py b/tests/test_hamiltonian.py index 6ed0b614..6a0a8883 100644 --- a/tests/test_hamiltonian.py +++ b/tests/test_hamiltonian.py @@ -279,7 +279,7 @@ def test_grad_compute_local_energy(trexio_file): Both functions compute e_L = T + V. compute_local_energy uses the custom VJP in compute_grads_and_laplacian_ln_Det (and _ln_det_bwd), while _compute_local_energy_auto uses a fully-automatic Laplacian via JAX second-order AD. The gradients w.r.t. - all Hamiltonian pytree leaves (lambda_matrix, Jastrow params, positions, …) must + all Hamiltonian pytree leaves (lambda_matrix, Jastrow params, positions, ...) must be numerically identical for a well-conditioned geminal matrix. """ seed = 123 diff --git a/tests/test_init_electron_configurations.py b/tests/test_init_electron_configurations.py index be7f0671..238c0b18 100644 --- a/tests/test_init_electron_configurations.py +++ b/tests/test_init_electron_configurations.py @@ -161,7 +161,7 @@ def test_per_atom_spin_counts_match_reference(name, charges, coords, tot_up, tot # Identical-atom dimers in the global singlet (S=0) configuration. The expected # physical behavior at any separation: each atom carries zeta electrons with # Hund-maximum local moment, anti-aligned to its partner so the global S=0. -# E.g., for N2 (zeta=7 each) at S=0 → one atom (4u, 3d), the other (3u, 4d). +# E.g., for N2 (zeta=7 each) at S=0 -> one atom (4u, 3d), the other (3u, 4d). @pytest.mark.parametrize("elem_z", [3, 6, 7, 8]) # Li, C, N, O (AE valence counts) @pytest.mark.parametrize("separation", [0.5, 2.0, 5.0, 100.0]) def test_dimer_singlet_atoms_are_antialigned(elem_z, separation): @@ -299,7 +299,7 @@ def test_dimer_per_atom_counts_match_reference_for_all_S(label, zeta_pair): ) -# Triatomic (linear, well-separated) chain — exercises ion_seq logic for >2 atoms. +# Triatomic (linear, well-separated) chain -- exercises ion_seq logic for >2 atoms. @pytest.mark.parametrize("elem_z", [3, 7]) def test_linear_triatomic_charge_neutrality(elem_z): """For a homonuclear linear triatomic at separation 5 bohr each, every atom diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index 1bcf6723..bf969b5f 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -574,7 +574,7 @@ def test_Jastrow_threebody_part_with_MOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_sphe_to_cart_AOs_data(): - """Round-trip AOs l<=6: spherical→Cartesian keeps J3 values/grads.""" + """Round-trip AOs l<=6: spherical->Cartesian keeps J3 values/grads.""" atol, rtol = get_tolerance("jastrow_eval", "strict") rng = np.random.default_rng(321) @@ -644,7 +644,7 @@ def test_Jastrow_threebody_part_sphe_to_cart_AOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_cart_to_sphe_AOs_data(): - """Round-trip AOs l<=6: Cartesian→spherical keeps J3 values/grads.""" + """Round-trip AOs l<=6: Cartesian->spherical keeps J3 values/grads.""" atol, rtol = get_tolerance("jastrow_eval", "strict") rng = np.random.default_rng(654) @@ -714,7 +714,7 @@ def test_Jastrow_threebody_part_cart_to_sphe_AOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_sphe_to_cart_MOs_data(): - """Round-trip MOs built on l<=6 AOs: spherical→Cartesian keeps J3 values/grads.""" + """Round-trip MOs built on l<=6 AOs: spherical->Cartesian keeps J3 values/grads.""" atol, rtol = get_tolerance("jastrow_eval", "strict") rng = np.random.default_rng(777) @@ -791,7 +791,7 @@ def test_Jastrow_threebody_part_sphe_to_cart_MOs_data(): @pytest.mark.activate_if_skip_heavy def test_Jastrow_threebody_part_cart_to_sphe_MOs_data(): - """Round-trip MOs l<=6: Cartesian→spherical keeps J3 values/grads.""" + """Round-trip MOs l<=6: Cartesian->spherical keeps J3 values/grads.""" atol, rtol = get_tolerance("jastrow_eval", "strict") rng = np.random.default_rng(888) @@ -1105,7 +1105,7 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): """Analytic vs auto-diff gradients/laplacian for three-body Jastrow (AOs).""" # J3 grad/lap crosses two zones: jastrow_grad_lap (fp32 mixed) + ao_grad_lap (fp64). - # Use the looser of the two — under mixed precision, jastrow_grad_lap dominates. + # Use the looser of the two -- under mixed precision, jastrow_grad_lap dominates. atol, rtol = get_tolerance_min(["jastrow_grad_lap", "ao_grad_lap"], "strict") num_r_up_cart_samples = 4 num_r_dn_cart_samples = 2 @@ -1253,7 +1253,7 @@ def test_analytic_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): # --------------------------------------------------------------------------- -# Jastrow combination matrix for "part" tests (J1+J2+J3±NN) +# Jastrow combination matrix for "part" tests (J1+J2+J3+/-NN) # Each tuple is (j1b_type, j2b_type, include_nn). # --------------------------------------------------------------------------- _JASTROW_COMBOS = [ @@ -1491,7 +1491,7 @@ def _make_jastrow_with_j3(j_matrix): def test_symmetrize_j3_symmetric_subblock(): - """L1-7: [:, :-1] symmetric → 0.5*(A+A.T) on sub-block, last col unchanged.""" + """L1-7: [:, :-1] symmetric -> 0.5*(A+A.T) on sub-block, last col unchanged.""" rng = np.random.RandomState(10) n = 4 sq_sym = rng.randn(n, n) @@ -1507,7 +1507,7 @@ def test_symmetrize_j3_symmetric_subblock(): def test_symmetrize_j3_nonsymmetric_subblock(): - """L1-8: [:, :-1] non-symmetric → no-op.""" + """L1-8: [:, :-1] non-symmetric -> no-op.""" rng = np.random.RandomState(11) n = 4 sq_nonsym = rng.randn(n, n) @@ -1522,7 +1522,7 @@ def test_symmetrize_j3_nonsymmetric_subblock(): def test_symmetrize_j3_none(): - """L1-9: jastrow_three_body_data=None → no-op.""" + """L1-9: jastrow_three_body_data=None -> no-op.""" jd = Jastrow_data() # all components are None mat = np.random.RandomState(12).randn(3, 4) result = jd.symmetrize_j3(mat) @@ -1530,7 +1530,7 @@ def test_symmetrize_j3_none(): def test_symmetrize_j3_too_few_columns(): - """L1-10: j_matrix with shape[1] < 2 → no-op.""" + """L1-10: j_matrix with shape[1] < 2 -> no-op.""" j_matrix = np.array([[1.0], [2.0], [3.0]]) # (3, 1) jd = _make_jastrow_with_j3(j_matrix) mat = np.random.RandomState(13).randn(3, 1) @@ -1678,9 +1678,9 @@ def test_streaming_J2_state_against_full(j2b_type, n_up, n_dn): full computation) within strict tolerance. J2 is electron-pair coupled, so the advance updates the moved electron's - same-spin row (i ≠ k) and the cross-spin partners. Sign asymmetry between - σ=up and σ=dn cross branches makes this the most error-prone of the - streaming kernels — exercise both branches with K=32 alternating moves. + same-spin row (i != k) and the cross-spin partners. Sign asymmetry between + sigma=up and sigma=dn cross branches makes this the most error-prone of the + streaming kernels -- exercise both branches with K=32 alternating moves. """ from jqmc.jastrow_factor import ( _advance_grads_laplacian_Jastrow_two_body_streaming_state, @@ -1781,7 +1781,7 @@ def test_streaming_J3_state_against_full(trexio_file): K = 32 # J3 streaming crosses two zones: jastrow_grad_lap (J3 grad/lap arithmetic) # and ao_grad_lap (AO grad/lap consumed inside J3). Use the looser of the - # two — under mixed precision, jastrow_grad_lap (fp32) dominates. + # two -- under mixed precision, jastrow_grad_lap (fp32) dominates. atol, rtol = get_tolerance_min(["jastrow_grad_lap", "ao_grad_lap"], "strict") for _ in range(K): # pick a random single-electron move (alternating spins when available) diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index a0fb4635..968ad0a3 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -502,7 +502,7 @@ def make_mcmc_with_patches(mcmc_instance: MCMC): "lambda_matrix": True, }, }, - # ── AO basis optimization cases ── + # -- AO basis optimization cases -- { "name": "j3_basis_exp_only", "flags": dict( @@ -635,7 +635,7 @@ def make_mcmc_with_patches(mcmc_instance: MCMC): jax.clear_caches() - # ── use_lm (LM/aSR) smoke test ────────────────────────────────────────── + # -- use_lm (LM/aSR) smoke test ------------------------------------------ # A separate MCMC instance with comput_e_L_param_deriv=True. # get_aH is patched at instance level so that no real sampled data are # needed; the dummy return values have H_1 < 0 so compute_asr_gamma @@ -760,7 +760,7 @@ def test_sr_wide_and_tall_matrix(trexio_file, regime, cg_flag, monkeypatch): mcmc_seed = 12345 epsilon_AS = 1.0e-6 - # Build base parameter registry — j1, j2, lambda. + # Build base parameter registry -- j1, j2, lambda. # For the "tall" regime, pad lambda_matrix so that total_params stays # larger than num_samples_total even with multiple MPI ranks. base_params = {} @@ -805,7 +805,7 @@ def test_sr_wide_and_tall_matrix(trexio_file, regime, cg_flag, monkeypatch): fake_w_L_data = np.ones((num_mcmc, num_walkers)) fake_e_L_data = rng.standard_normal((num_mcmc, num_walkers)) * 0.1 - # ── monkeypatch helpers ────────────────────────────────────────────────── + # -- monkeypatch helpers -------------------------------------------------- params_registry: dict[int, dict[str, np.ndarray]] = {} def register_params(wf, params): @@ -890,7 +890,7 @@ def fake_get_gF( monkeypatch.setattr(MCMC, "w_L", property(lambda self: fake_w_L_data), raising=False) monkeypatch.setattr(MCMC, "e_L", property(lambda self: fake_e_L_data), raising=False) - # ── run the test ───────────────────────────────────────────────────────── + # -- run the test --------------------------------------------------------- mcmc = MCMC( hamiltonian_data=hamiltonian_data, Dt=Dt, @@ -954,7 +954,7 @@ def test_opt_with_projected_MOs(trexio_file, monkeypatch): store_tuple=True, ) - # Minimal 2-body Jastrow — no 3-body/NN to keep the test fast. + # Minimal 2-body Jastrow -- no 3-body/NN to keep the test fast. jastrow_data = Jastrow_data( jastrow_one_body_data=None, jastrow_two_body_data=Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="exp"), @@ -1054,18 +1054,18 @@ def fake_get_gF( # --------------------------------------------------------------------------- -# L3: VMC optimization loop — symmetry preservation tests +# L3: VMC optimization loop -- symmetry preservation tests # --------------------------------------------------------------------------- # Test parameters: (j3_type, lambda_type) _SYMMETRY_TEST_CASES = [ # L3-1: baseline, both symmetric, all params ("sym", "square_sym"), - # L3-5: j3 symmetric, lambda non-symmetric → only j3 preserved + # L3-5: j3 symmetric, lambda non-symmetric -> only j3 preserved ("sym", "square_nonsym"), - # L3-6: j3 non-symmetric, lambda symmetric → only lambda preserved + # L3-6: j3 non-symmetric, lambda symmetric -> only lambda preserved ("nonsym", "square_sym"), - # L3-7: both non-symmetric → no symmetrization (no-op) + # L3-7: both non-symmetric -> no symmetrization (no-op) ("nonsym", "square_nonsym"), # L3-8: j3 symmetric, rectangular lambda with paired-symmetric sub-block ("sym", "rect_paired_sym"), @@ -1091,7 +1091,7 @@ def test_vmc_symmetry_preservation(j3_type, lambda_type, monkeypatch): coulomb_potential_data, ) = read_trexio_file(trexio_file=trexio_file, store_tuple=True) - # ── Build j3 matrix ────────────────────────────────────────────────────── + # -- Build j3 matrix ------------------------------------------------------ rng = np.random.RandomState(42) n_orb = aos_data._num_orb if j3_type == "sym": @@ -1106,7 +1106,7 @@ def test_vmc_symmetry_preservation(j3_type, lambda_type, monkeypatch): jastrow_threebody_data = Jastrow_three_body_data(orb_data=aos_data, j_matrix=j3_matrix) jastrow_data = Jastrow_data(jastrow_three_body_data=jastrow_threebody_data) - # ── Build lambda matrix ────────────────────────────────────────────────── + # -- Build lambda matrix -------------------------------------------------- n_up_elec = geminal_mo_data.num_electron_up n_dn_elec = geminal_mo_data.num_electron_dn orb_num = geminal_mo_data.orb_num_up # = orb_num_dn for MO geminals @@ -1151,8 +1151,8 @@ def test_vmc_symmetry_preservation(j3_type, lambda_type, monkeypatch): num_walkers = 2 - # ── Mock only the sampling/energy; leave get_variational_blocks and - # apply_block_updates real so symmetrize_metric is exercised. ──────── + # -- Mock only the sampling/energy; leave get_variational_blocks and + # apply_block_updates real so symmetrize_metric is exercised. -------- def fake_run(self, num_mcmc_steps=0, max_time=None): return None @@ -1226,7 +1226,7 @@ def fake_get_gF( j3_after = np.asarray(mcmc.hamiltonian_data.wavefunction_data.jastrow_data.jastrow_three_body_data.j_matrix) lam_after = np.asarray(mcmc.hamiltonian_data.wavefunction_data.geminal_data.lambda_matrix) - # ── Assertions ─────────────────────────────────────────────────────────── + # -- Assertions ----------------------------------------------------------- # j3 / lambda_matrix live in jastrow_eval / det_eval zones; symmetry is a structural # property of the matrix itself, so use those zones' tolerances. atol, rtol = get_tolerance_min(("jastrow_eval", "det_eval"), "strict") @@ -1442,7 +1442,7 @@ def test_epsilon_cutoff(self): assert np.isfinite(E_lm) def test_all_diag_S_zero(self): - """All diag(S) = 0 → dgelscut removes all parameters → zero update, E_lm == H_0.""" + """All diag(S) = 0 -> dgelscut removes all parameters -> zero update, E_lm == H_0.""" p = 3 H_0 = -1.5 f_vec = np.ones(p) * 0.1 @@ -1473,7 +1473,7 @@ def test_v0_max_selection(self): def test_optimize_lm_e2e_smoke(): - """End-to-end LM optimisation — verifies no NaN/Inf after 1 step.""" + """End-to-end LM optimisation -- verifies no NaN/Inf after 1 step.""" trexio_file = os.path.join(os.path.dirname(__file__), "trexio_example_files", "H2_ae_ccpvdz_cart.h5") ( structure_data, diff --git a/tests/test_jqmc_tool.py b/tests/test_jqmc_tool.py index f6ed9c8a..ea13ea4c 100644 --- a/tests/test_jqmc_tool.py +++ b/tests/test_jqmc_tool.py @@ -423,7 +423,7 @@ def _write_chk(path, objs): meta.attrs["driver_type"] = "MCMC" meta.attrs["mpi_size"] = len(objs) - # hamiltonian_data — minimal stub for force tests + # hamiltonian_data -- minimal stub for force tests first = objs[0] if hasattr(first, "hamiltonian_data"): _write_minimal_hamiltonian(f, first.hamiltonian_data) diff --git a/tests/test_mixed_precision.py b/tests/test_mixed_precision.py index 546a5091..3f9b1968 100644 --- a/tests/test_mixed_precision.py +++ b/tests/test_mixed_precision.py @@ -171,7 +171,7 @@ def _assert_eval_shape_dtype(fn, expected, label, *args, **kwargs): # --------------------------------------------------------------------------- -# A. AO zone (orb_eval → float32 in mixed) +# A. AO zone (orb_eval -> float32 in mixed) # --------------------------------------------------------------------------- @@ -202,7 +202,7 @@ def test_compute_AOs_laplacian_output_dtype(self, h2_data): # --------------------------------------------------------------------------- -# B. MO zone (orb_eval → float32 in mixed) +# B. MO zone (orb_eval -> float32 in mixed) # --------------------------------------------------------------------------- @@ -228,7 +228,7 @@ def test_compute_MOs_output_dtype(self, h2_data): # --------------------------------------------------------------------------- -# C. Jastrow zone (jastrow → float32 in mixed) +# C. Jastrow zone (jastrow -> float32 in mixed) # --------------------------------------------------------------------------- @@ -280,7 +280,7 @@ def test_jastrow_part_output_dtype(self, h2_data): # --------------------------------------------------------------------------- -# D. Geminal zone (geminal → float32 in mixed) +# D. Geminal zone (geminal -> float32 in mixed) # --------------------------------------------------------------------------- @@ -298,7 +298,7 @@ def test_geminal_matrix_output_dtype(self, h2_data): # --------------------------------------------------------------------------- -# E. Determinant zone (determinant → float64 in mixed) +# E. Determinant zone (determinant -> float64 in mixed) # --------------------------------------------------------------------------- @@ -329,7 +329,7 @@ def test_compute_grads_and_laplacian_ln_Det_output_dtype(self, h2_data): # --------------------------------------------------------------------------- -# F. Coulomb zone (coulomb → float32 in mixed) +# F. Coulomb zone (coulomb -> float32 in mixed) # --------------------------------------------------------------------------- @@ -368,7 +368,7 @@ def test_bare_coulomb_total_output_dtype(self, h2_data): # --------------------------------------------------------------------------- -# G. Kinetic zone (kinetic → float64 in mixed) +# G. Kinetic zone (kinetic -> float64 in mixed) # --------------------------------------------------------------------------- @@ -471,12 +471,12 @@ def water_data(self): return _load_trexio("water_ccecp_ccpvqz.h5") def test_geminal_up_one_row_output_dtype(self, water_data): - # Use [0:1] to get shape (1, 3) — compute_orb_api requires (N, 3), not (3,) + # Use [0:1] to get shape (1, 3) -- compute_orb_api requires (N, 3), not (3,) row = compute_geminal_up_one_row_elements(water_data["geminal_data"], water_data["r_up"][0:1], water_data["r_dn"]) _assert_dtype(row, get_dtype_jnp("det_ratio"), "compute_geminal_up_one_row_elements") def test_geminal_dn_one_column_output_dtype(self, water_data): - # Use [0:1] to get shape (1, 3) — compute_orb_api requires (N, 3), not (3,) + # Use [0:1] to get shape (1, 3) -- compute_orb_api requires (N, 3), not (3,) col = compute_geminal_dn_one_column_elements(water_data["geminal_data"], water_data["r_up"], water_data["r_dn"][0:1]) _assert_dtype(col, get_dtype_jnp("det_ratio"), "compute_geminal_dn_one_column_elements") diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index f7c51de3..c9eac251 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -202,9 +202,9 @@ def test_kinetic_energy_analytic_and_auto(trexio_file: str): def test_debug_and_auto_kinetic_energy_all_elements(trexio_file: str): """Debug vs autodiff kinetic energy per-electron arrays. - The debug path computes ``-1/2 · ∇²Psi / Psi`` via central finite differences + The debug path computes ``-1/2 * nabla^2Psi / Psi`` via central finite differences on Psi (h = 2e-4); under mixed precision the fp32 round-off in ao_eval / - jastrow_eval propagates into Psi at ~1e-7 and is amplified by 1/h² = 2.5e7, + jastrow_eval propagates into Psi at ~1e-7 and is amplified by 1/h^2 = 2.5e7, giving an O(1) relative error in the FD Laplacian. Marked ``numerical_diff`` so conftest skips it under ``--precision-mode=mixed``. """ @@ -761,7 +761,7 @@ def _streaming_step_consistency_one(wavefunction_data, r_up0, r_dn0, K, atol, rt moved_index = idx # rebuild A_inv at the new configuration (mirrors what Sherman-Morrison - # produces in the GFMC loop, modulo round-off — comparing at the same + # produces in the GFMC loop, modulo round-off -- comparing at the same # numerical reference here). A_inv = _build_A_inv_from_carts(wavefunction_data.geminal_data, jnp.asarray(r_up), jnp.asarray(r_dn)) state = _advance_kinetic_energy_all_elements_streaming_state( From 0715b7dc79e715c479be4fe5969bbe07ba665eec Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 00:53:48 +0900 Subject: [PATCH 44/97] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad250783..bcbfbf5b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,10 @@ # See https://pre-commit.com for more informatio # See https://pre-commit.com/hooks.html for more hooks + +# Restrict hooks to the pre-commit stage by default; only hooks with an +# explicit `stages: [commit-msg]` (or other) run on those events. +default_stages: [pre-commit] + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 From be6e7a690d45813a1fc754c01f1696c1addabf32 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 01:00:10 +0900 Subject: [PATCH 45/97] Implement jqmc-lint-ruff.yml. --- .github/workflows/jqmc-lint-ruff.yml | 40 +++++++++++++++++++++ .github/workflows/jqmc-run-full-pytest.yml | 12 ------- .github/workflows/jqmc-run-long-pytest.yml | 12 ------- .github/workflows/jqmc-run-rc-pytest.yml | 12 ------- .github/workflows/jqmc-run-short-pytest.yml | 12 ------- 5 files changed, 40 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/jqmc-lint-ruff.yml diff --git a/.github/workflows/jqmc-lint-ruff.yml b/.github/workflows/jqmc-lint-ruff.yml new file mode 100644 index 00000000..8c65f6f2 --- /dev/null +++ b/.github/workflows/jqmc-lint-ruff.yml @@ -0,0 +1,40 @@ +# Lint workflow: runs the same pre-commit hooks declared in +# .pre-commit-config.yaml (ruff, ruff-format, ascii-only, etc.) on every +# push and pull request to any branch. Enforces the same rules locally +# and in CI, so contributors who skip `pre-commit install` are still +# caught here. +# +# To enforce additional ruff rules, fix the violations listed in +# `lint.extend-ignore` in pyproject.toml and remove them from that list. + +name: jqmc lint (ruff + pre-commit) + +on: + push: + branches: [ "**" ] + pull_request: + branches: [ "**" ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.12" + + - name: Cache pre-commit envs + uses: actions/cache@v3 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + pre-commit-${{ runner.os }}- + + - name: Lint (pre-commit, currently-enforced hooks) + run: | + python -m pip install pre-commit + pre-commit run --all-files diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index abf4f66a..187207bd 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -47,18 +47,6 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - - name: Lint (pre-commit, currently-enforced hooks) - # Runs hooks declared in .pre-commit-config.yaml: - # - ruff (RUF001/002/003 ambiguous-unicode only, --no-fix) - # - ruff-format - # - ascii-only (scripts/check_ascii.sh) - # - trailing-whitespace, end-of-file-fixer, check-added-large-files - # To enforce additional ruff rules, fix the violations listed in - # `lint.extend-ignore` in pyproject.toml and remove them from that list. - run: | - python -m pip install pre-commit - pre-commit run --all-files - - name: Test jqmc FP64 (intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail diff --git a/.github/workflows/jqmc-run-long-pytest.yml b/.github/workflows/jqmc-run-long-pytest.yml index 6696289f..89f4e3db 100644 --- a/.github/workflows/jqmc-run-long-pytest.yml +++ b/.github/workflows/jqmc-run-long-pytest.yml @@ -47,18 +47,6 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - - name: Lint (pre-commit, currently-enforced hooks) - # Runs hooks declared in .pre-commit-config.yaml: - # - ruff (RUF001/002/003 ambiguous-unicode only, --no-fix) - # - ruff-format - # - ascii-only (scripts/check_ascii.sh) - # - trailing-whitespace, end-of-file-fixer, check-added-large-files - # To enforce additional ruff rules, fix the violations listed in - # `lint.extend-ignore` in pyproject.toml and remove them from that list. - run: | - python -m pip install pre-commit - pre-commit run --all-files - - name: Test jqmc FP64/FP32+FP64 (intra-software comparisons) run: | pytest -s -v tests/test_trexio.py diff --git a/.github/workflows/jqmc-run-rc-pytest.yml b/.github/workflows/jqmc-run-rc-pytest.yml index 1a5009fa..95f8b433 100644 --- a/.github/workflows/jqmc-run-rc-pytest.yml +++ b/.github/workflows/jqmc-run-rc-pytest.yml @@ -42,18 +42,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Lint (pre-commit, currently-enforced hooks) - # Runs hooks declared in .pre-commit-config.yaml: - # - ruff (RUF001/002/003 ambiguous-unicode only, --no-fix) - # - ruff-format - # - ascii-only (scripts/check_ascii.sh) - # - trailing-whitespace, end-of-file-fixer, check-added-large-files - # To enforce additional ruff rules, fix the violations listed in - # `lint.extend-ignore` in pyproject.toml and remove them from that list. - run: | - python -m pip install pre-commit - pre-commit run --all-files - - name: Install jqmc run: | python -m pip install flake8 pytest pytest-cov diff --git a/.github/workflows/jqmc-run-short-pytest.yml b/.github/workflows/jqmc-run-short-pytest.yml index d4d00807..2e40103f 100644 --- a/.github/workflows/jqmc-run-short-pytest.yml +++ b/.github/workflows/jqmc-run-short-pytest.yml @@ -58,18 +58,6 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - - name: Lint (pre-commit, currently-enforced hooks) - # Runs hooks declared in .pre-commit-config.yaml: - # - ruff (RUF001/002/003 ambiguous-unicode only, --no-fix) - # - ruff-format - # - ascii-only (scripts/check_ascii.sh) - # - trailing-whitespace, end-of-file-fixer, check-added-large-files - # To enforce additional ruff rules, fix the violations listed in - # `lint.extend-ignore` in pyproject.toml and remove them from that list. - run: | - python -m pip install pre-commit - pre-commit run --all-files - - name: Test jqmc FP64 (intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --skip-heavy From 39bbb24c866845f22296136d5ce387a43eb3be39 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 01:25:50 +0900 Subject: [PATCH 46/97] ruff: apply the first autofix --- .pre-commit-config.yaml | 4 +- CONTRIBUTING.md | 6 +- README.md | 2 +- benchmarks/benchmark_gfmc_kernels.py | 2 +- benchmarks/benchmark_mcmc_kernels.py | 2 +- doc/api_reference_cli.md | 24 ++-- doc/api_reference_cli_tool.md | 28 ++--- doc/changelog.md | 12 +- doc/examples.md | 124 ++++++++++---------- doc/notes/ao.md | 10 +- doc/notes/lrdmc.md | 8 +- doc/notes/mixed_precision.md | 54 ++++----- doc/notes/vmc.md | 14 +-- doc/notes/wavefunction.md | 10 +- doc/notes/workflows.md | 130 ++++++++++----------- doc/overview.md | 2 +- examples/jqmc-example04/README.md | 24 ++-- examples/jqmc-workflow-example01/README.md | 28 ++--- examples/jqmc-workflow-example02/README.md | 52 ++++----- examples/jqmc-workflow-example03/README.md | 20 ++-- jqmc/coulomb_potential.py | 4 +- jqmc/jqmc_gfmc.py | 2 +- jqmc/jqmc_mcmc.py | 2 +- jqmc/molecular_orbital.py | 2 +- pyproject.toml | 4 +- tests/test_AOs.py | 8 +- tests/test_MOs.py | 10 +- tests/test_ao_basis_optimization.py | 14 +-- tests/test_comparison_with_turborvb_AE.py | 10 +- tests/test_comparison_with_turborvb_ECP.py | 14 +-- tests/test_determinant.py | 14 +-- tests/test_ecps.py | 12 +- tests/test_hamiltonian.py | 14 +-- tests/test_jastrow.py | 12 +- tests/test_jqmc_command_lines.py | 4 +- tests/test_jqmc_gfmc_bra.py | 12 +- tests/test_jqmc_gfmc_tau.py | 12 +- tests/test_jqmc_mcmc.py | 14 +-- tests/test_jqmc_tool.py | 6 +- tests/test_lrdmc_force.py | 14 +-- tests/test_mcmc_force.py | 12 +- tests/test_mixed_precision.py | 18 +-- tests/test_swct.py | 6 +- tests/test_trexio.py | 2 +- tests/test_wave_function.py | 10 +- 45 files changed, 394 insertions(+), 394 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bcbfbf5b..04a2d812 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,10 +24,10 @@ repos: - repo: local hooks: - id: ascii-only - name: Reject non-ASCII bytes in Python sources + name: Reject non-ASCII bytes in source / config / docs entry: scripts/check_ascii.sh language: system - types: [python] + types_or: [python, toml, yaml, markdown, shell] exclude: '^(jqmc/obsolete/|prototypes/)' pass_filenames: true - id: ascii-only-commit-msg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f851a79e..6bc76bce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,8 +6,8 @@ The following items provide guidance for developers who wish to contribute to `j In `jQMC`, our top priorities are: -1. **Sustainability** – maintainability of the codebase -2. **Ease of development** – simplicity of implementing new features or theories +1. **Sustainability** - maintainability of the codebase +2. **Ease of development** - simplicity of implementing new features or theories We are willing to sacrifice some computational speed to achieve these goals. To that end, please follow these guidelines when contributing: @@ -21,7 +21,7 @@ We are willing to sacrifice some computational speed to achieve these goals. To * **Data** are defined as static classes using `flax.struct.dataclass`. * **Algorithms** are implemented as standalone functions that accept these dataclass instances as arguments. * Related dataclasses and their algorithms live in the same `Python` file and module, preserving the spirit of OOP while ensuring clarity. -* This design not only improves readability but also aligns with `JAX`’s requirement for side-effect-free functions. +* This design not only improves readability but also aligns with `JAX`'s requirement for side-effect-free functions. --- diff --git a/README.md b/README.md index bb4cd8d8..548291a1 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ What sets **jQMC** apart: - Written in `Python`, **jQMC** is designed to be user-friendly for executing simulations and easily extensible for developers implementing and testing new QMC methods. - By leveraging `JAX` just-in-time (`jit`) compilation and vectorized mapping (`vmap`) functionalities, the code achieves high-performance computations **especially on GPUs** while remaining portable across CPUs, GPUs, and TPUs. - MPI support enables the execution of large-scale computations on HPC facilities. -- **Automated workflows**: The `jqmc-workflow` module automates the entire simulation pipeline — from pilot runs and step-count estimation through production runs and convergence monitoring — allowing users to obtain publication-quality results with minimal manual intervention. +- **Automated workflows**: The `jqmc-workflow` module automates the entire simulation pipeline -- from pilot runs and step-count estimation through production runs and convergence monitoring -- allowing users to obtain publication-quality results with minimal manual intervention. - To minimize bugs, the code is written in a loosely coupled manner and includes comprehensive unit tests and regression tests (managed by `pytest`). This combination of features makes **jQMC** a versatile and powerful tool for both users and developers in the field of quantum Monte Carlo simulations. diff --git a/benchmarks/benchmark_gfmc_kernels.py b/benchmarks/benchmark_gfmc_kernels.py index ff2595b9..e6b0a6a7 100644 --- a/benchmarks/benchmark_gfmc_kernels.py +++ b/benchmarks/benchmark_gfmc_kernels.py @@ -116,7 +116,7 @@ def _load_cudart(): # Try ``pip install nvtx`` package first (bundles its own .so), # then fall back to ctypes libnvToolsExt.so from CUDA toolkit. try: - import nvtx as _nvtx_mod # noqa: F811 + import nvtx as _nvtx_mod print("[INFO] NVTX ranges enabled via ``nvtx`` pip package.") except ImportError: diff --git a/benchmarks/benchmark_mcmc_kernels.py b/benchmarks/benchmark_mcmc_kernels.py index 4e6b5c38..0e7893ab 100644 --- a/benchmarks/benchmark_mcmc_kernels.py +++ b/benchmarks/benchmark_mcmc_kernels.py @@ -94,7 +94,7 @@ def _load_cudart(): # Try ``pip install nvtx`` package first (bundles its own .so), # then fall back to ctypes libnvToolsExt.so from CUDA toolkit. try: - import nvtx as _nvtx_mod # noqa: F811 + import nvtx as _nvtx_mod print("[INFO] NVTX ranges enabled via ``nvtx`` pip package.") except ImportError: diff --git a/doc/api_reference_cli.md b/doc/api_reference_cli.md index aa98d412..611017fe 100644 --- a/doc/api_reference_cli.md +++ b/doc/api_reference_cli.md @@ -17,7 +17,7 @@ mpirun -np jqmc > The input file is a JSON/YAML document whose keys match the parameters listed below. > **Note** -> Throughout this document, “per MPI process and per walker” means the quantity is counted for each MPI rank and for each walker on that rank. When relevant, the total across all ranks and walkers is indicated explicitly. +> Throughout this document, "per MPI process and per walker" means the quantity is counted for each MPI rank and for each walker on that rank. When relevant, the total across all ranks and walkers is indicated explicitly. --- @@ -42,12 +42,12 @@ The input file is a JSON/YAML document whose keys match the parameters listed be | Key | Default | Description | | -------------------------- | -----------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `num_mcmc_steps` | **required** | Number of **measurement** steps per MPI process and per walker. Local energy and other observables are measured `num_mcmc_steps` times in total per (rank × walker). The global total equals `num_mcmc_steps × mpi_size × number_of_walkers`. | +| `num_mcmc_steps` | **required** | Number of **measurement** steps per MPI process and per walker. Local energy and other observables are measured `num_mcmc_steps` times in total per (rank x walker). The global total equals `num_mcmc_steps x mpi_size x number_of_walkers`. | | `num_mcmc_per_measurement` | `40` | MCMC updates between successive measurements. Observables are recorded every `num_mcmc_per_measurement` steps. | | `num_mcmc_warmup_steps` | `0` | Number of **warm-up** measurement steps to be discarded. | -| `num_mcmc_bin_blocks` | `1` | Number of binning blocks per MPI process and per walker (total binned blocks = `num_mcmc_bin_blocks × mpi_size × number_of_walkers`). | +| `num_mcmc_bin_blocks` | `1` | Number of binning blocks per MPI process and per walker (total binned blocks = `num_mcmc_bin_blocks x mpi_size x number_of_walkers`). | | `Dt` | `2.0` | MCMC step size (Bohr). | -| `epsilon_AS` | `0.0` | ε parameter for the Attaccalite–Sorella regularization. | +| `epsilon_AS` | `0.0` | eps parameter for the Attaccalite-Sorella regularization. | | `atomic_force` | `false` | If `true`, compute atomic forces. | | `use_swct` | `true` | If `true`, apply Space Warp Coordinate Transformation (SWCT) to atomic forces. Default is `true` for MCMC. | @@ -62,15 +62,15 @@ The input file is a JSON/YAML document whose keys match the parameters listed be | `num_mcmc_warmup_steps` | `0` | Warm-up steps to discard. | | `num_mcmc_bin_blocks` | `1` | Binning blocks per MPI process and per walker. | | `Dt` | `2.0` | MCMC step size (Bohr). | -| `epsilon_AS` | `0.0` | ε for Attaccalite–Sorella regularization. | +| `epsilon_AS` | `0.0` | eps for Attaccalite-Sorella regularization. | | `num_opt_steps` | **required** | Number of optimization iterations. | | `wf_dump_freq` | `1` | Write wavefunction/Hamiltonian checkpoint every this many optimization steps. | -| `optimizer_kwargs` | `{ "method": "sr", "delta": 0.01, "epsilon": 0.001, "cg_flag": true, "cg_max_iter": 10000, "cg_tol": 1e-4 }` | Optimizer configuration. Set `method` to `"sr"` for stochastic reconfiguration or to an optax optimizer name (e.g., `"adam"`). For `method = "sr"`: `delta`/`epsilon` control step size and regularization; `cg_*` entries tune the conjugate-gradient solver. To use the linear method, set `method = "sr"` with `use_lm = true`; `delta` controls the learning rate, `epsilon` controls SR regularization, `lm_subspace_dim` (default `0`) sets the LM subspace dimension (`0`=aSR, `>0`=LM with top-N params, `-1`=all params), and `lm_cond` (default `0.001`) sets the dgelscut correlation matrix min eigenvalue threshold (condition number ≤ 1/lm_cond). Any additional keys are forwarded to optax when `method` is an optax optimizer name. | +| `optimizer_kwargs` | `{ "method": "sr", "delta": 0.01, "epsilon": 0.001, "cg_flag": true, "cg_max_iter": 10000, "cg_tol": 1e-4 }` | Optimizer configuration. Set `method` to `"sr"` for stochastic reconfiguration or to an optax optimizer name (e.g., `"adam"`). For `method = "sr"`: `delta`/`epsilon` control step size and regularization; `cg_*` entries tune the conjugate-gradient solver. To use the linear method, set `method = "sr"` with `use_lm = true`; `delta` controls the learning rate, `epsilon` controls SR regularization, `lm_subspace_dim` (default `0`) sets the LM subspace dimension (`0`=aSR, `>0`=LM with top-N params, `-1`=all params), and `lm_cond` (default `0.001`) sets the dgelscut correlation matrix min eigenvalue threshold (condition number <= 1/lm_cond). Any additional keys are forwarded to optax when `method` is an optax optimizer name. | | `opt_J1_param` | `true` | Optimize J1 parameters. | | `opt_J2_param` | `true` | Optimize J2 parameters. | | `opt_J3_param` | `true` | Optimize J3 parameters. | | `opt_JNN_param` | `false` | Optimize neural-network Jastrow parameters. | -| `opt_lambda_param` | `false` | Optimize geminal (λ) parameters. | +| `opt_lambda_param` | `false` | Optimize geminal (lambda) parameters. | | `opt_with_projected_MOs` | `false` | If `true`, optimize lambda parameters (for AOs) or molecular orbital coefficients (for MOs) in a restricted MO space. | | `opt_J3_basis_exp` | `false` | Optimize J3 AO Gaussian exponents. Can be combined with `opt_with_projected_MOs`. | | `opt_J3_basis_coeff` | `false` | Optimize J3 AO contraction coefficients. Can be combined with `opt_with_projected_MOs`. | @@ -85,15 +85,15 @@ The input file is a JSON/YAML document whose keys match the parameters listed be | -------------------------- | -----------: | -------------------------------------------------------------------------------------------------------------------------------------- | | `num_mcmc_steps` | **required** | Number of **measurement** steps per MPI process and per walker during LRDMC. | | `num_mcmc_per_measurement` | `40` | Number of GFMC projections between measurements (observables recorded after each block of projections). | -| `alat` | `0.30` | Lattice discretization parameter (grid spacing). The lattice spacing is `alat × a₀`, where `a₀` is the Bohr radius. | +| `alat` | `0.30` | Lattice discretization parameter (grid spacing). The lattice spacing is `alat x a_0`, where `a_0` is the Bohr radius. | | `non_local_move` | `"tmove"` | Treatment of non-local ECP terms: `"tmove"` (T-move) or `"dltmove"` (determinant-locality + T-move). | | `num_gfmc_warmup_steps` | `0` | Number of warm-up measurement steps to discard. | -| `num_gfmc_bin_blocks` | `1` | Number of binning blocks for GFMC. **Total binned blocks = `num_gfmc_bin_blocks`** (not multiplied by `mpi_size × number_of_walkers`). | +| `num_gfmc_bin_blocks` | `1` | Number of binning blocks for GFMC. **Total binned blocks = `num_gfmc_bin_blocks`** (not multiplied by `mpi_size x number_of_walkers`). | | `num_gfmc_collect_steps` | `0` | Number of pre-binning measurements used to collect/accumulate weights. | | `E_scf` | `0.0` | Initial total-energy guess used to set the initial GFMC energy shift. | | `atomic_force` | `false` | If `true`, compute atomic forces. | | `use_swct` | `false` | If `true`, apply Space Warp Coordinate Transformation (SWCT) to atomic forces. Default is `false` for LRDMC. | -| `epsilon_PW` | `0.0` | Pathak–Wagner regularization parameter (Bohr). When > 0, the force estimator is regularized near the nodal surface (no regularization by default). | +| `epsilon_PW` | `0.0` | Pathak-Wagner regularization parameter (Bohr). When > 0, the force estimator is regularized near the nodal surface (no regularization by default). | --- @@ -103,14 +103,14 @@ The input file is a JSON/YAML document whose keys match the parameters listed be | ------------------------ | -----------: | ---------------------------------------------------------------------- | | `num_mcmc_steps` | **required** | Number of **measurement** steps per MPI process and per walker. | | `tau` | `0.10` | Imaginary-time step size between projections. | -| `alat` | `0.30` | Lattice discretization parameter; lattice spacing `alat × a₀`. | +| `alat` | `0.30` | Lattice discretization parameter; lattice spacing `alat x a_0`. | | `non_local_move` | `"tmove"` | Non-local ECP treatment: `"tmove"` or `"dltmove"`. | | `num_gfmc_warmup_steps` | `0` | Warm-up steps to discard. | | `num_gfmc_bin_blocks` | `1` | Binning blocks for GFMC (total binned blocks = `num_gfmc_bin_blocks`). | | `num_gfmc_collect_steps` | `0` | Pre-binning measurement count for weight collection. | | `atomic_force` | `false` | If `true`, compute atomic forces. | | `use_swct` | `false` | If `true`, apply SWCT to atomic forces. Default is `false` for LRDMC. | -| `epsilon_PW` | `0.0` | Pathak–Wagner regularization parameter (Bohr). When > 0, the force estimator is regularized near the nodal surface (no regularization by default). | +| `epsilon_PW` | `0.0` | Pathak-Wagner regularization parameter (Bohr). When > 0, the force estimator is regularized near the nodal surface (no regularization by default). | --- diff --git a/doc/api_reference_cli_tool.md b/doc/api_reference_cli_tool.md index e63d07b1..6d499f6c 100644 --- a/doc/api_reference_cli_tool.md +++ b/doc/api_reference_cli_tool.md @@ -62,7 +62,7 @@ jqmc-tool hamiltonian show-info ``` ### `to-xyz` -Export nuclear geometry to XYZ (Bohr -> Å). +Export nuclear geometry to XYZ (Bohr -> A). ``` jqmc-tool hamiltonian to-xyz [-o struct.xyz] @@ -74,7 +74,7 @@ Convert wavefunction ansatz inside a Hamiltonian file. ``` jqmc-tool hamiltonian conv-wf -c {jsd|jagp} [-o output.h5] ``` -- `-c, --convert-to`: `jagp` converts SD→AGP (AOs); `jsd` is not implemented. +- `-c, --convert-to`: `jagp` converts SD->AGP (AOs); `jsd` is not implemented. --- @@ -104,17 +104,17 @@ Jackknife estimator of VMC energy from an MCMC restart archive. ``` jqmc-tool mcmc compute-energy [-b N] [-w W] ``` -- `-b, --num_mcmc_bin_blocks` (int, default 1): Binning blocks per MPI × walker; total blocks = `b * mpi_size * walkers`. Must be ≥ `MCMC_MIN_BIN_BLOCKS`. -- `-w, --num_mcmc_warmup_steps` (int, default 0): Discarded warmup measurements; must be ≥ `MCMC_MIN_WARMUP_STEPS`. +- `-b, --num_mcmc_bin_blocks` (int, default 1): Binning blocks per MPI x walker; total blocks = `b * mpi_size * walkers`. Must be >= `MCMC_MIN_BIN_BLOCKS`. +- `-w, --num_mcmc_warmup_steps` (int, default 0): Discarded warmup measurements; must be >= `MCMC_MIN_WARMUP_STEPS`. ### `compute-force` -Jackknife estimator of VMC atomic forces (Hellmann–Feynman + Pulay) from an MCMC restart archive. Requires that the MCMC run was performed with `atomic_force = true`. +Jackknife estimator of VMC atomic forces (Hellmann-Feynman + Pulay) from an MCMC restart archive. Requires that the MCMC run was performed with `atomic_force = true`. ``` jqmc-tool mcmc compute-force [-b N] [-w W] ``` -- `-b, --num_mcmc_bin_blocks` (int, default 1): Binning blocks per MPI × walker; total blocks = `b * mpi_size * walkers`. Must be ≥ `MCMC_MIN_BIN_BLOCKS`. -- `-w, --num_mcmc_warmup_steps` (int, default 0): Discarded warmup measurements; must be ≥ `MCMC_MIN_WARMUP_STEPS`. +- `-b, --num_mcmc_bin_blocks` (int, default 1): Binning blocks per MPI x walker; total blocks = `b * mpi_size * walkers`. Must be >= `MCMC_MIN_BIN_BLOCKS`. +- `-w, --num_mcmc_warmup_steps` (int, default 0): Discarded warmup measurements; must be >= `MCMC_MIN_WARMUP_STEPS`. Outputs a per-atom force table in Ha/bohr with jackknife error bars. @@ -135,19 +135,19 @@ Jackknife estimator of LRDMC energy from an LRDMC restart archive. ``` jqmc-tool lrdmc compute-energy [-b N] [-w W] [-c C] ``` -- `-b, --num_gfmc_bin_blocks` (int, default 5): Binning blocks per MPI × walker (note: total blocks = `b`, not multiplied by ranks × walkers). Must be ≥ `GFMC_MIN_BIN_BLOCKS`. -- `-w, --num_gfmc_warmup_steps` (int, default 0): Discarded warmup steps; must be ≥ `GFMC_MIN_WARMUP_STEPS`. -- `-c, --num_gfmc_collect_steps` (int, default 5): Pre-binning measurements used to collect weights; must be ≥ `GFMC_MIN_COLLECT_STEPS`. +- `-b, --num_gfmc_bin_blocks` (int, default 5): Binning blocks per MPI x walker (note: total blocks = `b`, not multiplied by ranks x walkers). Must be >= `GFMC_MIN_BIN_BLOCKS`. +- `-w, --num_gfmc_warmup_steps` (int, default 0): Discarded warmup steps; must be >= `GFMC_MIN_WARMUP_STEPS`. +- `-c, --num_gfmc_collect_steps` (int, default 5): Pre-binning measurements used to collect weights; must be >= `GFMC_MIN_COLLECT_STEPS`. ### `compute-force` -Jackknife estimator of LRDMC atomic forces (Hellmann–Feynman + Pulay) from an LRDMC restart archive. Requires that the LRDMC run was performed with `atomic_force = true`. +Jackknife estimator of LRDMC atomic forces (Hellmann-Feynman + Pulay) from an LRDMC restart archive. Requires that the LRDMC run was performed with `atomic_force = true`. ``` jqmc-tool lrdmc compute-force [-b N] [-w W] [-c C] ``` -- `-b, --num_gfmc_bin_blocks` (int, default 5): Binning blocks per MPI × walker. Must be ≥ `GFMC_MIN_BIN_BLOCKS`. -- `-w, --num_gfmc_warmup_steps` (int, default 0): Discarded warmup steps; must be ≥ `GFMC_MIN_WARMUP_STEPS`. -- `-c, --num_gfmc_collect_steps` (int, default 5): Pre-binning measurements used to collect weights; must be ≥ `GFMC_MIN_COLLECT_STEPS`. +- `-b, --num_gfmc_bin_blocks` (int, default 5): Binning blocks per MPI x walker. Must be >= `GFMC_MIN_BIN_BLOCKS`. +- `-w, --num_gfmc_warmup_steps` (int, default 0): Discarded warmup steps; must be >= `GFMC_MIN_WARMUP_STEPS`. +- `-c, --num_gfmc_collect_steps` (int, default 5): Pre-binning measurements used to collect weights; must be >= `GFMC_MIN_COLLECT_STEPS`. Outputs a per-atom force table in Ha/bohr with jackknife error bars. diff --git a/doc/changelog.md b/doc/changelog.md index 6d96b501..ac69f25a 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -61,10 +61,10 @@ This release focuses on a major update of the VMC optimizer (Linear Method), ext * **GFMC_t projection averaging**: Fixed incorrect averaging of the number of projections across MPI ranks in GFMC_t. * **SR with `num_params >= num_samples`**: Fixed MPI bug when the number of optimizable parameters exceeds the number of samples. * **MPI `Allreduce` for scalars**: Replaced `Allreduce` with `allreduce` for scalar `int` and `float` values in `jqmc_mcmc.py` and `jqmc_gfmc.py`, as `Allreduce` for scalars exhibits implementation-dependent behavior. -* **Optimizer step estimation**: Fixed `estimate_required_steps` — removed incorrect `ceil` rounding and `max` clamp that ignored `walker_ratio`; added `min_steps` parameter. +* **Optimizer step estimation**: Fixed `estimate_required_steps` -- removed incorrect `ceil` rounding and `max` clamp that ignored `walker_ratio`; added `min_steps` parameter. * **SR stability near convergence**: Improved stability of SR with adaptive learning rate in the vicinity of convergence. * **Pytree inconsistency**: Fixed a JAX pytree structural mismatch. -* **S/N ratio diagnostics**: Fixed averaging (last S/N ratio → averaged S/N ratios) and trivial output bugs. +* **S/N ratio diagnostics**: Fixed averaging (last S/N ratio -> averaged S/N ratios) and trivial output bugs. ### Workflow (`jqmc_workflow`) @@ -111,10 +111,10 @@ This is a major update with drastic performance improvements, new features, and ### New features -* **LRDMC force calculations**: Implemented LRDMC atomic forces with the Pathak–Wagner regularization. +* **LRDMC force calculations**: Implemented LRDMC atomic forces with the Pathak-Wagner regularization. * **Jastrow functions**: Added `jastrow_1b_type` (`'exp'` / `'pade'`) and `jastrow_2b_type` (`'pade'` / `'exp'`) fields to `Jastrow_one_body_data` and `Jastrow_two_body_data`, enabling runtime selection of the one-body and two-body Jastrow functional forms. * Exponential form: $u(r) = \frac{1}{2b}(1 - e^{-br})$ - * Padé form: $u(r) = \frac{r}{2(1 + br)}$ + * Pade form: $u(r) = \frac{r}{2(1 + br)}$ ### Bug fixes @@ -126,10 +126,10 @@ This is a major update with drastic performance improvements, new features, and ### Infrastructure * **Restart file format change**: Switched restart files from pickle-based `*.chk` to HDF5-based `*.h5`. **Note:** backward compatibility with old `*.chk` files is *not* maintained. -* **`jqmc_workflow` package**: Introduced the `jqmc_workflow` automation package for orchestrating multi-stage QMC pipelines (WF conversion → VMC optimization → MCMC / LRDMC production) with automatic step estimation, checkpointing, and remote job management. +* **`jqmc_workflow` package**: Introduced the `jqmc_workflow` automation package for orchestrating multi-stage QMC pipelines (WF conversion -> VMC optimization -> MCMC / LRDMC production) with automatic step estimation, checkpointing, and remote job management. * **Removed `SWCT_data`**: Cleaned up legacy `SWCT_data` class as part of codebase refactoring. * **More comprehensive tests**: Substantially expanded the test suite to cover the new features and improve overall reliability. -* **Expanded examples**: Reorganized and enriched the `examples/` directory with 11 end-to-end tutorials (`jqmc-example01`–`jqmc-example08`, `jqmc-workflow-example01`–`jqmc-workflow-example03`) covering single-point VMC/LRDMC, force calculations, GPU walker-scaling benchmarks, interaction-energy workflows, and PES scans with automated `jqmc_workflow` pipelines. +* **Expanded examples**: Reorganized and enriched the `examples/` directory with 11 end-to-end tutorials (`jqmc-example01`-`jqmc-example08`, `jqmc-workflow-example01`-`jqmc-workflow-example03`) covering single-point VMC/LRDMC, force calculations, GPU walker-scaling benchmarks, interaction-energy workflows, and PES scans with automated `jqmc_workflow` pipelines. ## Feb-5-2026: v0.1.0 diff --git a/doc/examples.md b/doc/examples.md index 8820a80e..57474d38 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -1026,18 +1026,18 @@ The directory structure will look like: ``` water_dimer_qmc/ -├── 01_S22_water_monomer_1/ # monomer 1 -│ └── 01DFT/ -├── 02_S22_water_monomer_2/ # monomer 2 -│ └── 01DFT/ -└── 03_S22_water_dimer/ # dimer - ├── 01DFT/ - ├── 02vmc_JSD/ - ├── 03mcmc_JSD/ - ├── 04lrdmc_JSD/ - ├── 05vmc_JAGP/ - ├── 06mcmc_JAGP/ - └── 07lrdmc_JAGP/ +--- 01_S22_water_monomer_1/ # monomer 1 +- --- 01DFT/ +--- 02_S22_water_monomer_2/ # monomer 2 +- --- 01DFT/ +--- 03_S22_water_dimer/ # dimer + --- 01DFT/ + --- 02vmc_JSD/ + --- 03mcmc_JSD/ + --- 04lrdmc_JSD/ + --- 05vmc_JAGP/ + --- 06mcmc_JAGP/ + --- 07lrdmc_JAGP/ ``` ### Generate trial WFs (DFT) @@ -2444,17 +2444,17 @@ After running the pipeline, each bond length has the following structure: ``` R_0.74/ -├── 00_pyscf/ # pySCF DFT calculation -│ ├── run_pyscf.py -│ ├── H2_R_0.74.h5 # TREXIO file -│ └── H2_R_0.74.out # pySCF output -├── 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 -├── 02_vmc/ # VMC_Workflow: Jastrow + MO optimization -│ ├── hamiltonian_data_opt_step_1.h5 -│ ├── ... -│ └── hamiltonian_data_opt_step_20.h5 -├── 03_mcmc/ # MCMC_Workflow: production sampling + forces -└── 04_lrdmc/ # LRDMC_Workflow: LRDMC (a=0.2) + forces +--- 00_pyscf/ # pySCF DFT calculation +- --- run_pyscf.py +- --- H2_R_0.74.h5 # TREXIO file +- --- H2_R_0.74.out # pySCF output +--- 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 +--- 02_vmc/ # VMC_Workflow: Jastrow + MO optimization +- --- hamiltonian_data_opt_step_1.h5 +- --- ... +- --- hamiltonian_data_opt_step_20.h5 +--- 03_mcmc/ # MCMC_Workflow: production sampling + forces +--- 04_lrdmc/ # LRDMC_Workflow: LRDMC (a=0.2) + forces ``` ### Workflow DAG @@ -2462,8 +2462,8 @@ R_0.74/ For each R, the dependency graph is: ``` -pySCF --> WF --> VMC ─┬─--> MCMC - └─--> LRDMC (a=0.2) +pySCF --> WF --> VMC -----> MCMC + ----> LRDMC (a=0.2) ``` All 20 R values are independent and execute in parallel via `Launcher`. @@ -2510,7 +2510,7 @@ The script prints a summary table after all calculations complete: ### Results -![H2 PES — MCMC and LRDMC](../examples/jqmc-workflow-example01/H2_PES_mcmc_lrdmc.png) +![H2 PES -- MCMC and LRDMC](../examples/jqmc-workflow-example01/H2_PES_mcmc_lrdmc.png) ### References @@ -2665,35 +2665,35 @@ After running the pipeline: ``` jqmc-workflow-example02/ -├── run_pipelines.py # Main script -├── run_pyscf.py # Standalone pySCF script (reference) -├── water_trexio.hdf5 # TREXIO file (pySCF output) -├── jqmc_setting_local/ # Machine configuration -├── 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 -├── 02_vmc/ # VMC_Workflow: Jastrow optimization -├── 03_mcmc/ # MCMC production (per walker count) -│ ├── w00008/ -│ ├── w00016/ -│ ├── ... -│ └── w08192/ -├── 04_lrdmc/ # LRDMC production (per walker count) -│ ├── w00008/ -│ ├── w00016/ -│ ├── ... -│ └── w08192/ +--- run_pipelines.py # Main script +--- run_pyscf.py # Standalone pySCF script (reference) +--- water_trexio.hdf5 # TREXIO file (pySCF output) +--- jqmc_setting_local/ # Machine configuration +--- 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 +--- 02_vmc/ # VMC_Workflow: Jastrow optimization +--- 03_mcmc/ # MCMC production (per walker count) +- --- w00008/ +- --- w00016/ +- --- ... +- --- w08192/ +--- 04_lrdmc/ # LRDMC production (per walker count) +- --- w00008/ +- --- w00016/ +- --- ... +- --- w08192/ ``` ### Workflow DAG ``` -pySCF --> WF --> VMC ─┬─--> MCMC (w8) ─┐ - ├─--> MCMC (w16) │ - ├─--> ... ├─--> Summary table - ├─--> MCMC (w8192) │ - ├─--> LRDMC (w8) │ - ├─--> LRDMC (w16) │ - ├─--> ... │ - └─--> LRDMC (w8192) ─┘ +pySCF --> WF --> VMC -----> MCMC (w8) -- + ----> MCMC (w16) - + ----> ... ----> Summary table + ----> MCMC (w8192) - + ----> LRDMC (w8) - + ----> LRDMC (w16) - + ----> ... - + ----> LRDMC (w8192) -- ``` ### Machine configuration @@ -2702,8 +2702,8 @@ This example assumes a cluster where each node has 4 NVIDIA GPUs. The benchmark | Component | Specification | |-----------|---------------| -| CPU | Intel Xeon Platinum 8490H (Sapphire Rapids, 60 cores, 1.90–3.50 GHz) × 2 sockets | -| GPU | NVIDIA H100 (Hopper) × 4 sockets | +| CPU | Intel Xeon Platinum 8490H (Sapphire Rapids, 60 cores, 1.90-3.50 GHz) x 2 sockets | +| GPU | NVIDIA H100 (Hopper) x 4 sockets | To run on a different cluster, change `SERVER`, `QUEUE_LABEL_s`, and `QUEUE_LABEL_l` in `run_pipelines.py` and provide the appropriate machine configuration in `jqmc_setting_local/`. @@ -2882,21 +2882,21 @@ After running the pipeline: ``` jqmc-workflow-example03/ -├── run_pipelines.py # Main script -├── 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 -├── 02_vmc/ # VMC_Workflow: Jastrow optimization (100 steps) -│ ├── hamiltonian_data_opt_step_1.h5 -│ ├── ... -│ └── hamiltonian_data_opt_step_100.h5 -├── 03_mcmc/ # MCMC_Workflow: production sampling + forces -└── 04_lrdmc/ # LRDMC_Workflow: LRDMC (a=0.30) + forces +--- run_pipelines.py # Main script +--- 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 +--- 02_vmc/ # VMC_Workflow: Jastrow optimization (100 steps) +- --- hamiltonian_data_opt_step_1.h5 +- --- ... +- --- hamiltonian_data_opt_step_100.h5 +--- 03_mcmc/ # MCMC_Workflow: production sampling + forces +--- 04_lrdmc/ # LRDMC_Workflow: LRDMC (a=0.30) + forces ``` ### Workflow DAG ``` -pySCF --> WF --> VMC ─┬─--> MCMC (energy + force) - └─--> LRDMC (energy + force) +pySCF --> WF --> VMC -----> MCMC (energy + force) + ----> LRDMC (energy + force) ``` ### Machine configuration diff --git a/doc/notes/ao.md b/doc/notes/ao.md index 426c300a..1903e589 100644 --- a/doc/notes/ao.md +++ b/doc/notes/ao.md @@ -3,9 +3,9 @@ ## Atomic orbitals for the pairing function and the Jastrow factor -One of the most common choices for atomic orbitals in QMC is atom‑centered Gaussian‑type orbitals (GTOs). A primitive GTO $\psi_{l,m,\alpha}(\mathbf{r})$ can be constructed using either solid harmonics or Cartesian polynomial basis functions. +One of the most common choices for atomic orbitals in QMC is atom-centered Gaussian-type orbitals (GTOs). A primitive GTO $\psi_{l,m,\alpha}(\mathbf{r})$ can be constructed using either solid harmonics or Cartesian polynomial basis functions. -### Gaussian‑Type Orbitals with Solid‑Harmonic Basis +### Gaussian-Type Orbitals with Solid-Harmonic Basis Primitive orbitals with regular solid harmonics are given by: @@ -55,7 +55,7 @@ These two normalizations satisfy \frac{\mathcal{N}^{\rm solid}_{l,m,\alpha}}{\mathcal{N}^{\rm sphe}_{l,m,\alpha}} = \sqrt{\frac{2l+1}{4\pi}}. ``` -### Gaussian‑Type Orbitals with Cartesian Basis +### Gaussian-Type Orbitals with Cartesian Basis Primitive orbitals in the Cartesian basis are @@ -81,12 +81,12 @@ We define the total angular momentum as $l=n_x+n_y+n_z$. A basis of order $l$ i ### Practical Tip -In jQMC (JAX), Cartesian GTOs are computationally faster than spherical ones when using `jit` and `grad`, since they avoid branching logic by varying only polynomial exponents rather than basis‑function forms. +In jQMC (JAX), Cartesian GTOs are computationally faster than spherical ones when using `jit` and `grad`, since they avoid branching logic by varying only polynomial exponents rather than basis-function forms. ## Real Spherical and Solid Harmonics -The **real spherical harmonics** $\mathcal{Y}_{l,m}(\theta,\phi)$ are built from the complex spherical harmonics $Y_{l,m}(\theta,\phi)$ with the Condon–Shortley phase: +The **real spherical harmonics** $\mathcal{Y}_{l,m}(\theta,\phi)$ are built from the complex spherical harmonics $Y_{l,m}(\theta,\phi)$ with the Condon-Shortley phase: ```{math} :label: eq-real-spherical diff --git a/doc/notes/lrdmc.md b/doc/notes/lrdmc.md index ad36b8d6..86d780cf 100644 --- a/doc/notes/lrdmc.md +++ b/doc/notes/lrdmc.md @@ -8,14 +8,14 @@ Lattice regularized diffusion Monte Carlo (LRDMC), initially proposed by Casula[ ## Practical points -1. No time-step error: unlike standard DMC, LRDMC does not rely on a Suzuki–Trotter decomposition. Instead, a systematic bias comes from the finite lattice spacing $a$[^1]. To obtain an unbiased fixed-node (FN) energy, extrapolate results to $a \to 0$ using several lattice spacings; the $a \to 0$ extrapolation is typically smooth and well captured by low-order polynomial fits. +1. No time-step error: unlike standard DMC, LRDMC does not rely on a Suzuki-Trotter decomposition. Instead, a systematic bias comes from the finite lattice spacing $a$[^1]. To obtain an unbiased fixed-node (FN) energy, extrapolate results to $a \to 0$ using several lattice spacings; the $a \to 0$ extrapolation is typically smooth and well captured by low-order polynomial fits. 2. Consistency with DMC: after removing the controllable $a \to 0$ extrapolation, FN energies from LRDMC agree with standard DMC calculations[^5]. 3. Multiple mesh sizes: LRDMC can introduce two mesh sizes ($a$ and $a'$) so that regions near nuclei and valence regions are diffused appropriately[^1][^6]. This will be introduced into `jQMC` in future. 4. Variational principle with ECPs: LRDMC retains the variational principle even in the presence of effective core potentials, analogous to the T-move treatment in standard DMC[^1][^7][^8]. `jQMC` implements an LRDMC algorithm that maintains parallel efficiency for many walkers and many nodes via load-balancing across walkers[^9]. -For further algorithmic details, please see the textbook “Quantum Monte Carlo Approaches for Correlated Systems”[^10]. +For further algorithmic details, please see the textbook "Quantum Monte Carlo Approaches for Correlated Systems"[^10]. [^1]: M. Casula, C. Filippi, and S. Sorella, Phys. Rev. Lett. 95, 100201 (2005). DOI: [10.1103/PhysRevLett.95.100201](https://doi.org/10.1103/PhysRevLett.95.100201) [^2]: D. F. Ten Haaf, H. J. Van Bemmel, J. M. Van Leeuwen, W. Van Saarloos, and D. M. Ceperley, Phys. Rev. B 51, 13039 (1995). DOI: [10.1103/PhysRevB.51.13039](https://doi.org/10.1103/PhysRevB.51.13039) @@ -23,7 +23,7 @@ For further algorithmic details, please see the textbook “Quantum Monte Carlo [^4]: S. Sorella and L. Capriotti, Phys. Rev. B 61, 2599 (2000). DOI: [10.1103/PhysRevB.61.2599](https://doi.org/10.1103/PhysRevB.61.2599) [^5]: F. Della Pia, et al., J. Chem. Phys. 163, 104110 (2025). DOI: [10.1063/5.0272974](https://doi.org/10.1063/5.0272974) [^6]: K. Nakano, R. Maezono, and S. Sorella, Phys. Rev. B 101, 155106 (2020). DOI: [10.1103/PhysRevB.101.155106](https://doi.org/10.1103/PhysRevB.101.155106) -[^7]: K. Nakano, S. Sorella, D. Alfè, A. Zen, J. Chem. Theory Comput. 20, 4591-4604 (2024). DOI: [10.1021/acs.jctc.4c00139](https://doi.org/10.1021/acs.jctc.4c00139) +[^7]: K. Nakano, S. Sorella, D. Alfe, A. Zen, J. Chem. Theory Comput. 20, 4591-4604 (2024). DOI: [10.1021/acs.jctc.4c00139](https://doi.org/10.1021/acs.jctc.4c00139) [^8]: M. Casula, Phys. Rev. B 74, 161102 (2006). DOI: [10.1103/PhysRevB.74.161102](https://doi.org/10.1103/PhysRevB.74.161102) [^9]: K. Nakano, S. Sorella, M. Casula, J. Chem. Phys. 163, 194117 (2025). DOI: [10.1063/5.0296986](https://doi.org/10.1063/5.0296986) -[^10]: “Quantum Monte Carlo Approaches for Correlated Systems”, Cambridge University Press (2017). DOI: [10.1017/9781316417041](https://doi.org/10.1017/9781316417041) +[^10]: "Quantum Monte Carlo Approaches for Correlated Systems", Cambridge University Press (2017). DOI: [10.1017/9781316417041](https://doi.org/10.1017/9781316417041) diff --git a/doc/notes/mixed_precision.md b/doc/notes/mixed_precision.md index db612c0b..75671cbf 100644 --- a/doc/notes/mixed_precision.md +++ b/doc/notes/mixed_precision.md @@ -18,7 +18,7 @@ mode = "mixed" Or keep the default (all float64, backward compatible): ```toml -# [precision] section omitted → mode="full" +# [precision] section omitted -> mode="full" ``` ## Precision modes @@ -39,18 +39,18 @@ chosen mode. |--------------------|------------------------|--------|----------|----------|------------| | `ao_eval` | `atomic_orbital` | f64 | **f32** | low | core | | `ao_grad` | `atomic_orbital` | f64 | **f32** | low | core | -| `ao_lap` | `atomic_orbital` | f64 | f64 | high§ | core | +| `ao_lap` | `atomic_orbital` | f64 | f64 | highSection | core | | `mo_eval` | `molecular_orbital` | f64 | f64 | high\* | core | | `mo_grad` | `molecular_orbital` | f64 | f64 | high | core | | `mo_lap` | `molecular_orbital` | f64 | f64 | high | core | -| `jastrow_eval` | `jastrow_factor` | f64 | **f32** | low | core† | +| `jastrow_eval` | `jastrow_factor` | f64 | **f32** | low | core* | | `jastrow_grad_lap` | `jastrow_factor` | f64 | **f32** | low | core | -| `jastrow_ratio` | `jastrow_factor` | f64 | **f32** | low | indirect‡ | +| `jastrow_ratio` | `jastrow_factor` | f64 | **f32** | low | indirect** | | `det_eval` | `determinant` | f64 | f64 | high | core | | `det_grad_lap` | `determinant` | f64 | f64 | high | core | -| `det_ratio` | `determinant` | f64 | f64 | high | indirect‡ | +| `det_ratio` | `determinant` | f64 | f64 | high | indirect** | | `coulomb` | `coulomb_potential` | f64 | **f32** | low-med | core | -| `wf_eval` | `wavefunction` | f64 | f64 | high | core† | +| `wf_eval` | `wavefunction` | f64 | f64 | high | core* | | `wf_kinetic` | `wavefunction` | f64 | f64 | high | core | | `wf_ratio` | `wavefunction` | f64 | f64 | high | no | | `local_energy` | `hamiltonians` | f64 | f64 | high | core | @@ -61,23 +61,23 @@ the small `mo_coefficients @ aos` matmul runs in this zone, and its output feeds the determinant matrix where fp32 round-off is amplified by `log|det|`. -† `jastrow_eval` and `wf_eval` are on the E_L core path but their +* `jastrow_eval` and `wf_eval` are on the E_L core path but their forward values (J and ln|Psi|) do not enter the E_L formula directly (E_L depends on *derivatives* of ln|Psi|). Diagnostics show zero E_L bias when these zones alone are fp32. -‡ `det_ratio` and `jastrow_ratio` affect E_L **indirectly** through the +** `det_ratio` and `jastrow_ratio` affect E_L **indirectly** through the ECP non-local potential, which evaluates `Psi(R')/Psi(R)` on a quadrature grid via rank-1 ratio updates. In non-ECP systems these zones have no E_L impact. -§ `ao_lap` is kept fp64 in `mixed` mode because the analytic Laplacian +Section `ao_lap` is kept fp64 in `mixed` mode because the analytic Laplacian formula contains catastrophic-cancellation terms of the form -`4 Z² r² − 6 Z` and `(safe_div − 2 Z·base)² − safe_div² − 2 Z` that -amplify fp32 round-off into a force bias of order ~1 Ha/bohr in N₂ +`4 Z^2 r^2 - 6 Z` and `(safe_div - 2 Z*base)^2 - safe_div^2 - 2 Z` that +amplify fp32 round-off into a force bias of order ~1 Ha/bohr in N_2 (diagnostic `bug/fp32/diag_07_ao_grad_vs_lap_split.py`). The grad counterpart `ao_grad` has no such cancellation and is safe at fp32 -(max|dF| ≈ 5e-6 Ha/bohr). This is the only zone pair in jQMC where the +(max|dF| ~= 5e-6 Ha/bohr). This is the only zone pair in jQMC where the grad and Laplacian halves take different dtypes, motivating the split of the original `ao_grad_lap` zone into separate `ao_grad` / `ao_lap` zones. @@ -107,25 +107,25 @@ The implementation rests on **three** principles documented at the top of `jqmc/_precision.py`. Principle 3 is the most important in practice; almost every precision bug we have seen is a violation of 3a or 3b. -**Principle 1 — One Precision Zone is owned by exactly one module.** +**Principle 1 -- One Precision Zone is owned by exactly one module.** A zone (e.g. `ao_eval`, `coulomb`) is *defined and consumed* in a single -module. The mapping zone ↔ owning module is one-to-one. +module. The mapping zone <-> owning module is one-to-one. -**Principle 2 — A module may own multiple Precision Zones.** +**Principle 2 -- A module may own multiple Precision Zones.** Different code paths in the same module legitimately need different precisions (e.g. `ao_eval` vs `ao_grad` vs `ao_lap`, or `det_eval` vs `det_ratio`). Each zone is named for its *purpose*, not for its dtype. -**Principle 3 — Cast responsibility lies with the function that does +**Principle 3 -- Cast responsibility lies with the function that does arithmetic on the value, never with passthrough wrappers.** * **3a (frozen args).** Function arguments are *frozen*: the parameter name must not be rebound for the entire body of the function. Writing `arg = jnp.asarray(arg, dtype=...)` at the top of a function is forbidden - — it silently coerces the argument for every later use, including + -- it silently coerces the argument for every later use, including forwarding to other functions. When the function consumes `arg` as an arithmetic operand, the cast appears **inside the expression** - (`arg.astype(dtype)`), or — if the cast result is reused — through a + (`arg.astype(dtype)`), or -- if the cast result is reused -- through a *new* local variable (e.g. `arg_local = arg.astype(dtype)`). The original `arg` always remains frozen. @@ -134,7 +134,7 @@ arithmetic on the value, never with passthrough wrappers.** operand. Inputs and outputs of the function's arithmetic both live in its zone. For catastrophic cancellation (`r - R`): reconstruct the difference in the dtype the values were received in (the - caller-supplied precision — fp64 in jQMC because the upstream MCMC + caller-supplied precision -- fp64 in jQMC because the upstream MCMC walker state is fp64), then down-cast the result to the function's own zone. The principle is "use the caller-supplied precision," **not** "hardcode fp64." @@ -169,8 +169,8 @@ automatically. The exemptions (modules whose data is *always fp64 by construction*, independent of mode) are: -* `mcmc` / `gfmc` — MCMC and GFMC walker state. -* I/O modules — `structure`, `trexio_wrapper`, `_jqmc_utility`, +* `mcmc` / `gfmc` -- MCMC and GFMC walker state. +* I/O modules -- `structure`, `trexio_wrapper`, `_jqmc_utility`, `jqmc_tool`, and the `_load_dataclass_from_hdf5` / `_save_dataclass_to_hdf5` helpers in `hamiltonians`. On-disk numerical data (AO exponents/coefficients, nuclear coordinates, geminal @@ -179,7 +179,7 @@ independent of mode) are: * **Basis-data storage accessors.** `_*_jnp` properties on selectable-precision dataclasses whose underlying storage field is typed `npt.NDArray[np.float64]` are *lift-only* adapters - (numpy → `jax.Array`), not arithmetic. The dtype is fp64 by + (numpy -> `jax.Array`), not arithmetic. The dtype is fp64 by construction (storage is loaded from HDF5/TREXIO/optimizer output); the consumer is responsible for casting the lifted array to its own zone at the use site (Principle 3b). Concretely this covers @@ -194,14 +194,14 @@ independent of mode) are: See {py:mod}`jqmc._precision` for the programmatic API: -* `get_dtype_jnp(zone)` / `get_dtype_np(zone)` — return the JAX / NumPy +* `get_dtype_jnp(zone)` / `get_dtype_np(zone)` -- return the JAX / NumPy dtype currently assigned to *zone*. -* `get_eps(name, dtype)` — return a dtype-aware numerical-stability +* `get_eps(name, dtype)` -- return a dtype-aware numerical-stability constant (e.g. `"rcond_svd"`, `"stabilizing_ao"`). -* `configure(mode)` — programmatically switch the active precision mode. -* `get_tolerance(zone, level)` — return `(atol, rtol)` for tests, scaled +* `configure(mode)` -- programmatically switch the active precision mode. +* `get_tolerance(zone, level)` -- return `(atol, rtol)` for tests, scaled by the zone's current dtype (`level` = `"strict"` or `"loose"`). -* `get_tolerance_min(zones, level)` — return the loosest `(atol, rtol)` +* `get_tolerance_min(zones, level)` -- return the loosest `(atol, rtol)` across the given zones. Use this when a test compares two paths whose combined dtype span crosses multiple zones; the achievable agreement is bounded by the weakest zone on the path. diff --git a/doc/notes/vmc.md b/doc/notes/vmc.md index d0ee09e0..928473fc 100644 --- a/doc/notes/vmc.md +++ b/doc/notes/vmc.md @@ -64,9 +64,9 @@ This constitutes the Variational Monte Carlo (VMC) framework. The optimization of $\boldsymbol{\alpha}$ is challenging due to a complex energy landscape with statistical noise. jQMC leverages JAX automatic differentiation to compute energy derivatives and employs the **stochastic reconfiguration** method\~\cite{1998SOR,2007SOR} for efficient parameter updates. -### MCMC Sampling and Metropolis–Hastings +### MCMC Sampling and Metropolis-Hastings -jQMC uses a generalized Metropolis–Hastings algorithm to sample $\pi(\mathbf{x})$. A proposed move from $\vec{x}$ to $\vec{x}'$ is accepted with probability: +jQMC uses a generalized Metropolis-Hastings algorithm to sample $\pi(\mathbf{x})$. A proposed move from $\vec{x}$ to $\vec{x}'$ is accepted with probability: ```{math} P(\vec{x}\to\vec{x}') = \min\Bigl[1, \frac{|\Psi_T(\vec{x}')|^2}{|\Psi_T(\vec{x})|^2} \frac{T(\vec{x}'\to\vec{x})}{T(\vec{x}\to\vec{x}')},\Bigr] @@ -99,7 +99,7 @@ and similarly for $T(\vec{x}'\to\vec{x})$, giving the ratio: ### Reweighting and AS Regularization -jQMC implements the Attaccalite–Sorella (AS) reweighting to handle divergences near nodal surfaces\~\cite{2008ATT}. One samples a guiding distribution $\Pi_G$ defined by: +jQMC implements the Attaccalite-Sorella (AS) reweighting to handle divergences near nodal surfaces\~\cite{2008ATT}. One samples a guiding distribution $\Pi_G$ defined by: ```{math} \Psi_G(\mathbf{x}) = \frac{R^\varepsilon(\mathbf{x})}{R(\mathbf{x})}\Psi_T(\mathbf{x}) @@ -134,7 +134,7 @@ The parameter $\varepsilon$ is chosen so that the average weight $\langle\mathca ### AO Basis Exponent and Coefficient Optimization -In addition to optimizing the Jastrow and geminal (lambda) matrix parameters, jQMC supports variational optimization of the Gaussian-type orbital (GTO) basis set parameters — specifically the **exponents** $Z_\alpha$ and **contraction coefficients** $c_\alpha$ of the primitive GTOs that enter both the three-body Jastrow factor and the geminal pairing function. +In addition to optimizing the Jastrow and geminal (lambda) matrix parameters, jQMC supports variational optimization of the Gaussian-type orbital (GTO) basis set parameters -- specifically the **exponents** $Z_\alpha$ and **contraction coefficients** $c_\alpha$ of the primitive GTOs that enter both the three-body Jastrow factor and the geminal pairing function. #### Variational derivatives @@ -155,10 +155,10 @@ Within a given atom, a single *shell* (same nucleus, same angular momentum $l$, To preserve this physical constraint during optimization, jQMC enforces that **all primitives belonging to the same shell are updated identically**. This is implemented via the same `symmetrize_metric` mechanism used for the J3 and lambda matrix symmetry: -1. **Gradient symmetrization** — Before the SR solve, the signal-to-noise ratio of the force vector $f_k$ is averaged within each shell group. This ensures that all shell-mates receive the same effective gradient. -2. **Update enforcement** — After the additive parameter update, `apply_block_update` averages the updated values within each shell group, guaranteeing that shell-mates remain exactly equal even in the presence of floating-point rounding. +1. **Gradient symmetrization** -- Before the SR solve, the signal-to-noise ratio of the force vector $f_k$ is averaged within each shell group. This ensures that all shell-mates receive the same effective gradient. +2. **Update enforcement** -- After the additive parameter update, `apply_block_update` averages the updated values within each shell group, guaranteeing that shell-mates remain exactly equal even in the presence of floating-point rounding. -This approach is size-preserving: the optimizer always works with the full per-primitive parameter vector (no dimension reduction), which keeps the SR matrix construction identical to other variational blocks. All basis parameters are optimized simultaneously — there is no SN-ratio filter or parameter-count selection. +This approach is size-preserving: the optimizer always works with the full per-primitive parameter vector (no dimension reduction), which keeps the SR matrix construction identical to other variational blocks. All basis parameters are optimized simultaneously -- there is no SN-ratio filter or parameter-count selection. #### Restrictions diff --git a/doc/notes/wavefunction.md b/doc/notes/wavefunction.md index 7e2bff69..ad3d835c 100644 --- a/doc/notes/wavefunction.md +++ b/doc/notes/wavefunction.md @@ -72,7 +72,7 @@ $$ u\left( r \right) = \frac{ 1 }{2 b_{\text{ei}}} \left( {1 - {e^{ - r b_{\text{ei}}}}} \right) \,, $$ (onebody_u) -**Padé form** (`jastrow_1b_type='pade'`): +**Pade form** (`jastrow_1b_type='pade'`): $$ u\left( r \right) = \frac{r}{2(1 + b_{\text{ei}} \cdot r)} \,, @@ -90,7 +90,7 @@ where $v_{{\sigma _i},{\sigma _j}}$ is another simple bounded function. jQMC supports two functional forms for $v_{{\sigma _i},{\sigma _j}}$, selectable via the `jastrow_2b_type` parameter: -**Padé form** (`jastrow_2b_type='pade'`, default): +**Pade form** (`jastrow_2b_type='pade'`, default): $$ {v_{{\sigma _i},{\sigma _j}}}\left( {{r_{i,j}}} \right) = @@ -210,13 +210,13 @@ $$ We augment the Jastrow factor with a neural-network term $J_{\text{NN}}$ using a PauliNet-inspired GNN [Hermann et al., Nat. Chem. 12, 891 (2020)]. Inputs are electron coordinates $\{\mathbf{r}_i\}_{i=1}^{N_e}$ with spins $s_i \in \{\uparrow,\downarrow\}$, nuclear coordinates $\{\mathbf{R}_I\}_{I=1}^{N_n}$, and atomic numbers $\{Z_I\}$. The architecture is translationally and rotationally invariant and symmetric under exchange of electrons within each spin channel; fermionic antisymmetry is carried by the Slater/Geminal part. Variational parameters in this VMC setting are all trainable weights and biases of the neural networks described below, including spin embeddings, nuclear (species) embeddings, the message/receiver networks, and the readout network. ### Input Features -Geometry is encoded solely by scalar distances: electron–electron $r_{ij}=|\mathbf{r}_i-\mathbf{r}_j|$ and electron–nucleus $r_{iI}=|\mathbf{r}_i-\mathbf{R}_I|$ (nucleus–nucleus distances are constant under the Born–Oppenheimer approximation). Each distance is expanded into PhysNet-style radial basis functions (RBFs) parametrized in the [PauliNet paper](https://doi.org/10.1038/s41557-020-0544-y): +Geometry is encoded solely by scalar distances: electron-electron $r_{ij}=|\mathbf{r}_i-\mathbf{r}_j|$ and electron-nucleus $r_{iI}=|\mathbf{r}_i-\mathbf{R}_I|$ (nucleus-nucleus distances are constant under the Born-Oppenheimer approximation). Each distance is expanded into PhysNet-style radial basis functions (RBFs) parametrized in the [PauliNet paper](https://doi.org/10.1038/s41557-020-0544-y): $$ e_k(r) = r^2 \exp\!\left[-r - \frac{(r-\mu_k)^2}{\sigma_k^2}\right], $$ -where $\mu_k$ and $\sigma_k$ are hyperparameters determined by the grid points $q_k$. The prefactor $r^2$ makes the feature and its derivative vanish at $r=0$, so $J_{\text{NN}}$ preserves the electron–nucleus cusp enforced by analytic terms. The number of basis functions $K$ is set by `num_rbf`, and the parameter `cutoff` ($r_c$) determines the distribution range of the basis function centers (user inputs). The parameters are defined as $\mu_k = r_c q_k^2, \quad \sigma_k = \frac{1}{7}(1 + r_c q_k)$, where $\{q_k\}_{k=1}^K$ are $K$ points evenly spaced in the interval $(0, 1)$. +where $\mu_k$ and $\sigma_k$ are hyperparameters determined by the grid points $q_k$. The prefactor $r^2$ makes the feature and its derivative vanish at $r=0$, so $J_{\text{NN}}$ preserves the electron-nucleus cusp enforced by analytic terms. The number of basis functions $K$ is set by `num_rbf`, and the parameter `cutoff` ($r_c$) determines the distribution range of the basis function centers (user inputs). The parameters are defined as $\mu_k = r_c q_k^2, \quad \sigma_k = \frac{1}{7}(1 + r_c q_k)$, where $\{q_k\}_{k=1}^K$ are $K$ points evenly spaced in the interval $(0, 1)$. ![PauliNet RBF features](paulinet_rbf_plot.png) @@ -526,7 +526,7 @@ p = \end{cases} $$ -This equation shows that, to compute the probability $p$, one does not have to compute the full rank of $\det(A^{new})$, but we can just compute $\vec{u}$ using the new electron configuration and combine the computed $\vec{u}$ with the old matrix $\det(A^{old})$ that can be stored on a memory. Notice that, $\det(A^{new})$ itself is not needed to compute the probability, but one needs $\det(A^{new})^{-1}$ to compute the next probability. Therefore, in the standard VMC implementation, every time a proposed move is accepted, the matrix inverse is updated using a rank-1 update using the so-called {\it Sherman–Morrison} formula: +This equation shows that, to compute the probability $p$, one does not have to compute the full rank of $\det(A^{new})$, but we can just compute $\vec{u}$ using the new electron configuration and combine the computed $\vec{u}$ with the old matrix $\det(A^{old})$ that can be stored on a memory. Notice that, $\det(A^{new})$ itself is not needed to compute the probability, but one needs $\det(A^{new})^{-1}$ to compute the next probability. Therefore, in the standard VMC implementation, every time a proposed move is accepted, the matrix inverse is updated using a rank-1 update using the so-called {\it Sherman-Morrison} formula: $$ (A^{new})^{-1} = (A^{old}+\vec{u}\vec{v}_l^T)^{-1} = (A^{old})^{-1} - (A^{old})^{-1}\vec{u}(1+\vec{v}_l^T (A^{old})^{-1} \vec{u})^{-1} \vec{v}_l^T (A^{old})^{-1}. diff --git a/doc/notes/workflows.md b/doc/notes/workflows.md index 13d633db..3b468af9 100644 --- a/doc/notes/workflows.md +++ b/doc/notes/workflows.md @@ -5,27 +5,27 @@ The **jqmc_workflow** package provides an autonomous pipeline engine for **jQMC** calculations. -Users define a *pipeline* — a directed acyclic graph (DAG) of workflow steps — +Users define a *pipeline* -- a directed acyclic graph (DAG) of workflow steps -- and the engine takes care of: -- **Input generation** — TOML input files are created automatically from +- **Input generation** -- TOML input files are created automatically from explicit parameter values (with sensible defaults). -- **Data transfer** — Files are uploaded to (and downloaded from) remote +- **Data transfer** -- Files are uploaded to (and downloaded from) remote supercomputers via SSH/SFTP using [Paramiko](https://www.paramiko.org/). -- **Job submission, monitoring, and collection** — Jobs are submitted through +- **Job submission, monitoring, and collection** -- Jobs are submitted through the site's scheduler (PBS / Slurm / local `bash`), polled until completion, and output files are fetched back. -- **Dependency resolution** — The DAG-based `Launcher` identifies which +- **Dependency resolution** -- The DAG-based `Launcher` identifies which workflow steps are *ready* (all dependencies satisfied) and executes them **in parallel** using Python `asyncio` tasks. -- **Target error-bar estimation** — When a `target_error` (Ha) is specified, +- **Target error-bar estimation** -- When a `target_error` (Ha) is specified, a small *pilot* run is executed first; the statistical error is used to estimate the number of production steps required, together with the estimated wall time. -A single Python script can therefore express a full QMC pipeline — from -wavefunction preparation (TREXIO → `hamiltonian_data.h5`) through VMC -optimization, MCMC production sampling, and LRDMC extrapolation — and run it +A single Python script can therefore express a full QMC pipeline -- from +wavefunction preparation (TREXIO -> `hamiltonian_data.h5`) through VMC +optimization, MCMC production sampling, and LRDMC extrapolation -- and run it end-to-end with automatic restarts across interruptions. @@ -33,24 +33,24 @@ end-to-end with automatic restarts across interruptions. ```text run_pipeline.py - │ - ▼ - ┌────────┐ - │Launcher│ DAG executor (asyncio) - └──┬─────┘ - │ creates asyncio.Task per ready node - │ - ├──► Container("vmc") - │ └─► VMC_Workflow.configure() → .run() - │ - ├──► Container("mcmc-prod") ← runs in parallel - │ └─► MCMC_Workflow.configure() → .run() - │ - └──► Container("lrdmc-ext") ← runs in parallel - └─► LRDMC_Ext_Workflow.configure() → .run() - ├─► LRDMC_Workflow (alat=0.50) ┐ - ├─► LRDMC_Workflow (alat=0.40) ├ parallel - └─► LRDMC_Workflow (alat=0.25) ┘ + - + v + ---------- + -Launcher- DAG executor (asyncio) + ---------- + - creates asyncio.Task per ready node + - + ---> Container("vmc") + - --> VMC_Workflow.configure() -> .run() + - + ---> Container("mcmc-prod") <- runs in parallel + - --> MCMC_Workflow.configure() -> .run() + - + ---> Container("lrdmc-ext") <- runs in parallel + --> LRDMC_Ext_Workflow.configure() -> .run() + --> LRDMC_Workflow (alat=0.50) - + --> LRDMC_Workflow (alat=0.40) - parallel + --> LRDMC_Workflow (alat=0.25) - ``` ### Key components @@ -65,7 +65,7 @@ run_pipeline.py | `MCMC_Workflow` | Production energy sampling (`job_type=mcmc`). | | `LRDMC_Workflow` | Lattice-regularized diffusion Monte Carlo for a single $a$ value. | | `LRDMC_Ext_Workflow` | Runs multiple `LRDMC_Workflow` instances at different lattice spacings and performs $a^2 \to 0$ extrapolation. | -| `ScientificPhase` | Enum defining the scientific phases of a workflow session (INIT → SCF → WF_BUILD → VMC → MCMC → LRDMC → COMPLETED). See [Phase management](#phase-management). | +| `ScientificPhase` | Enum defining the scientific phases of a workflow session (INIT -> SCF -> WF_BUILD -> VMC -> MCMC -> LRDMC -> COMPLETED). See [Phase management](#phase-management). | | `WorkflowStatus` / `JobStatus` | Enums for workflow-level and per-job status values. See [Status enums](#status-enums). | @@ -79,13 +79,13 @@ to actual paths or values. Pass a **file** produced by an upstream workflow. `filename` can be: -- A **static string** — when the exact name is known at definition time: +- A **static string** -- when the exact name is known at definition time: ```python FileFrom("vmc", "hamiltonian_data_opt_step_9.h5") ``` -- A **`ValueFrom` object** — when the name is determined at runtime +- A **`ValueFrom` object** -- when the name is determined at runtime (e.g. VMC early convergence produces a step number that cannot be predicted): @@ -127,7 +127,7 @@ Pass a **file** produced by an upstream workflow. `filename` can be: #### `ValueFrom(label, key)` Pass a **scalar value** from an upstream workflow's `output_values` -dict. The available keys depend on the workflow class — see the +dict. The available keys depend on the workflow class -- see the table below. #### Available `output_values` keys @@ -194,8 +194,8 @@ via `pilot_queue_label` (defaults to `queue_label`). #### MCMC / VMC -1. **Pilot** — A short run of `pilot_steps` steps. -2. **Production** — Step count estimated from the pilot error bar. +1. **Pilot** -- A short run of `pilot_steps` steps. +2. **Production** -- Step count estimated from the pilot error bar. #### LRDMC @@ -203,16 +203,16 @@ LRDMC has an additional calibration stage to automatically determine `num_projection_per_measurement` (GFMC projections per measurement) from a `target_survived_walkers_ratio` (default 0.97): -1. **Calibration** (`_pilot_a/_pilot1` – `_pilot_a/_pilot3`, parallel) — +1. **Calibration** (`_pilot_a/_pilot1` - `_pilot_a/_pilot3`, parallel) -- Three short LRDMC runs with - `num_projection_per_measurement = Ne × k × (0.3/alat)²` (k=2,4,6; + `num_projection_per_measurement = Ne x k x (0.3/alat)^2` (k=2,4,6; $N_e$ is the total electron count). A quadratic is fit to the observed survived-walkers ratio and the optimal `num_projection_per_measurement` is determined. -2. **Error-bar pilot** (`_pilot_b`) — A run with the calibrated +2. **Error-bar pilot** (`_pilot_b`) -- A run with the calibrated `num_projection_per_measurement`; its error bar estimates the production step count. -3. **Production** (`_1`, `_2`, …) — Start from scratch, accumulate +3. **Production** (`_1`, `_2`, ...) -- Start from scratch, accumulate statistics until `target_error` is achieved. If `num_projection_per_measurement` is given explicitly, the calibration @@ -345,7 +345,7 @@ total number of walkers. The ratio $W_{\text{pilot}} / W_{\text{prod}}$ (the *walker ratio*) accounts for the pilot queue using fewer (or more) MPI processes than the production queue. The number of MPI processes is read from -`queue_data.toml` (`num_cores`, or `mpi_per_node × nodes` as +`queue_data.toml` (`num_cores`, or `mpi_per_node x nodes` as fallback). The total production steps are then @@ -450,13 +450,13 @@ MCMC_Workflow( - `cleanup_patterns` accepts a list of glob patterns (e.g. `["restart.h5", "hamiltonian_opt*.h5"]`). -- Matching is **recursive** — patterns are applied to the workflow +- Matching is **recursive** -- patterns are applied to the workflow directory **and** all subdirectories (e.g. `_pilot/`, `_pilot_a/`, `_pilot_b/`). - **Local files** matching the patterns are always deleted. - **Remote files** are deleted only when the workflow targets a remote machine (`server_machine_name` is not `"localhost"`). -- Cleanup runs **after** `CompletionStatus.OK` is confirmed — it never +- Cleanup runs **after** `CompletionStatus.OK` is confirmed -- it never touches files while the workflow might still need them for continuation. - Cleanup failures are logged as warnings and never cause a completed @@ -478,15 +478,15 @@ child `LRDMC_Workflow`. Every job is recorded in `workflow_state.toml` with a lifecycle: ```text -submitted → completed → fetched +submitted -> completed -> fetched ``` On restart, the engine checks each job's status: -- **`fetched`** — Input generation *and* submission are both skipped. -- **`submitted`** / **`completed`** — Input is not regenerated; the job +- **`fetched`** -- Input generation *and* submission are both skipped. +- **`submitted`** / **`completed`** -- Input is not regenerated; the job is resumed (polled or fetched). -- **No record** — A fresh input file is generated and the job is submitted. +- **No record** -- A fresh input file is generated and the job is submitted. This means a pipeline can be interrupted at any point (Ctrl-C, node failure, wall-time limit) and simply re-run; it will pick up exactly @@ -505,14 +505,14 @@ A workflow session progresses through a sequence of **scientific phases** defined by the `ScientificPhase` enum (module `_phase`): ```text -INIT → SCF → WF_BUILD → VMC_PILOT → VMC → MCMC_PILOT → MCMC - ↓ - COMPLETED ← LRDMC_FIT ← LRDMC ← LRDMC_PILOT +INIT -> SCF -> WF_BUILD -> VMC_PILOT -> VMC -> MCMC_PILOT -> MCMC + dn + COMPLETED <- LRDMC_FIT <- LRDMC <- LRDMC_PILOT ``` Not every pipeline uses every phase (e.g. a VMC-only pipeline skips LRDMC phases). The allowed transitions are defined in -`PHASE_TRANSITIONS` — for example, from `VMC` you may advance to +`PHASE_TRANSITIONS` -- for example, from `VMC` you may advance to `MCMC_PILOT`, `MCMC`, `LRDMC_PILOT`, `LRDMC`, or `COMPLETED`. Each phase has a list of **allowed actions** (`PHASE_ALLOWED_ACTIONS`) @@ -528,7 +528,7 @@ A set of **always-allowed actions** (`advance_phase`, `rollback_phase`, regardless of phase/status. The `require_action()` function enforces these rules at the boundary -between an MCP tool call and a workflow method — if the action is not +between an MCP tool call and a workflow method -- if the action is not permitted, a `ValueError` is raised immediately. @@ -570,7 +570,7 @@ Each `[[jobs]]` record contains: | `server_machine` | Machine name | | `status` | One of the `JobStatus` values above | | `submitted_at` | ISO 8601 timestamp | -| `step` | Step index (0 = pilot, 1, 2, … = production) | +| `step` | Step index (0 = pilot, 1, 2, ... = production) | | `run_id` | Short hex identifier for the job | | `completed_at` | ISO 8601 timestamp (set on completion) | | `fetched_at` | ISO 8601 timestamp (set on fetch) | @@ -614,7 +614,7 @@ WARNING: Inputs have changed but previous results are still present. Delete 'mcmc_prod/' to re-run with the updated inputs. ``` -The container is **not** automatically re-run — the user must +The container is **not** automatically re-run -- the user must manually delete the stale directory. This conservative approach avoids the risk of mixing old and new job data on the remote server. @@ -635,7 +635,7 @@ exception_type = "RuntimeError" traceback = "..." ``` -The engine records raw data only — failure classification and recovery +The engine records raw data only -- failure classification and recovery strategy are responsibilities of external tooling (e.g. an MCP agent). @@ -665,7 +665,7 @@ job_acct_command = "sacct -j 12345 --format=State,ExitCode,MaxRSS,Elapsed -P" job_acct_file = "job_accounting_12345.txt" ``` -No parsing or interpretation is performed — that responsibility belongs +No parsing or interpretation is performed -- that responsibility belongs to external tooling. If `jobacct` is not configured, the `job_acct_command` and `job_acct_file` fields are simply absent. @@ -698,8 +698,8 @@ Configuration files are managed in the following directory hierarchy. The engine looks for a project-local override first, then falls back to the user-global directory: -1. `./jqmc_setting_local/` — project-local override (if it exists in CWD) -2. `~/.jqmc_setting/` — user-global default +1. `./jqmc_setting_local/` -- project-local override (if it exists in CWD) +2. `~/.jqmc_setting/` -- user-global default On the very first run, if neither directory exists, the template shipped with the package is copied to `~/.jqmc_setting/` and the user is asked @@ -709,14 +709,14 @@ to edit it. ```text ~/.jqmc_setting/ -├── machine_data.yaml # Server machine definitions -├── localhost/ # Settings for localhost -│ ├── queue_data.toml -│ └── submit_mpi.sh # (name is user-defined) -├── my-cluster/ # Settings for a remote cluster (nickname) -│ ├── queue_data.toml -│ └── submit_mpi.sh -└── ... +--- machine_data.yaml # Server machine definitions +--- localhost/ # Settings for localhost +- --- queue_data.toml +- --- submit_mpi.sh # (name is user-defined) +--- my-cluster/ # Settings for a remote cluster (nickname) +- --- queue_data.toml +- --- submit_mpi.sh +--- ... ``` ### `machine_data.yaml` @@ -941,7 +941,7 @@ jqmc ${INPUT} > ${OUTPUT} 2>&1 ## Pipeline example -A minimal pipeline script that runs VMC → MCMC + LRDMC extrapolation: +A minimal pipeline script that runs VMC -> MCMC + LRDMC extrapolation: ```python from jqmc_workflow import ( @@ -1020,7 +1020,7 @@ dynamically through `ValueFrom("vmc", "optimized_hamiltonian")`, so the pipeline works correctly even when VMC converges early (e.g. step 91 instead of 150). Additionally, `lrdmc-ext` depends on `mcmc-prod` (via `ValueFrom` for `E_scf`), so the DAG becomes -VMC → MCMC → LRDMC-ext. The `target_survived_walkers_ratio` +VMC -> MCMC -> LRDMC-ext. The `target_survived_walkers_ratio` triggers automatic calibration of `num_projection_per_measurement` independently at each lattice spacing. All alat values run their calibration, error-bar pilot, and production phases in parallel. diff --git a/doc/overview.md b/doc/overview.md index cdf8e780..d7aa2a4b 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -1,6 +1,6 @@ # Overview -**jQMC** is an ab initio quantum Monte Carlo (QMC) simulation package developed entirely from scratch using Python and JAX. Originally designed for molecular systems—with future extensions planned for periodic systems—**jQMC** implements two well-established QMC algorithms: Variational Monte Carlo (VMC) and a robust and efficient variant of Diffusion Monte Carlo known as Lattice Regularized Diffusion Monte Carlo (LRDMC). By leveraging JAX just-in-time (jit) compilation and vectorized mapping (vmap) functionalities, jQMC achieves high-performance computations especially on GPUs while remaining portable across CPUs and GPUs. +**jQMC** is an ab initio quantum Monte Carlo (QMC) simulation package developed entirely from scratch using Python and JAX. Originally designed for molecular systems--with future extensions planned for periodic systems--**jQMC** implements two well-established QMC algorithms: Variational Monte Carlo (VMC) and a robust and efficient variant of Diffusion Monte Carlo known as Lattice Regularized Diffusion Monte Carlo (LRDMC). By leveraging JAX just-in-time (jit) compilation and vectorized mapping (vmap) functionalities, jQMC achieves high-performance computations especially on GPUs while remaining portable across CPUs and GPUs. ![license](https://img.shields.io/github/license/kousuke-nakano/jQMC) ![tag](https://img.shields.io/github/v/tag/kousuke-nakano/jQMC) diff --git a/examples/jqmc-example04/README.md b/examples/jqmc-example04/README.md index b725ff8b..b93e2dad 100644 --- a/examples/jqmc-example04/README.md +++ b/examples/jqmc-example04/README.md @@ -18,18 +18,18 @@ The directory structure will look like: ``` water_dimer_qmc/ -├── 01_S22_water_monomer_1/ # monomer 1 -│ └── 01DFT/ -├── 02_S22_water_monomer_2/ # monomer 2 -│ └── 01DFT/ -└── 03_S22_water_dimer/ # dimer - ├── 01DFT/ - ├── 02vmc_JSD/ - ├── 03mcmc_JSD/ - ├── 04lrdmc_JSD/ - ├── 05vmc_JAGP/ - ├── 06mcmc_JAGP/ - └── 07lrdmc_JAGP/ +--- 01_S22_water_monomer_1/ # monomer 1 +- --- 01DFT/ +--- 02_S22_water_monomer_2/ # monomer 2 +- --- 01DFT/ +--- 03_S22_water_dimer/ # dimer + --- 01DFT/ + --- 02vmc_JSD/ + --- 03mcmc_JSD/ + --- 04lrdmc_JSD/ + --- 05vmc_JAGP/ + --- 06mcmc_JAGP/ + --- 07lrdmc_JAGP/ ``` ## Generate trial WFs (DFT) diff --git a/examples/jqmc-workflow-example01/README.md b/examples/jqmc-workflow-example01/README.md index ae9397a0..88426c5b 100644 --- a/examples/jqmc-workflow-example01/README.md +++ b/examples/jqmc-workflow-example01/README.md @@ -128,17 +128,17 @@ After running the pipeline, each bond length has the following structure: ``` R_0.74/ -├── 00_pyscf/ # pySCF DFT calculation -│ ├── run_pyscf.py -│ ├── H2_R_0.74.h5 # TREXIO file -│ └── H2_R_0.74.out # pySCF output -├── 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 -├── 02_vmc/ # VMC_Workflow: Jastrow + MO optimization -│ ├── hamiltonian_data_opt_step_1.h5 -│ ├── ... -│ └── hamiltonian_data_opt_step_20.h5 -├── 03_mcmc/ # MCMC_Workflow: production sampling + forces -└── 04_lrdmc/ # LRDMC_Workflow: LRDMC (a=0.2) + forces +--- 00_pyscf/ # pySCF DFT calculation +- --- run_pyscf.py +- --- H2_R_0.74.h5 # TREXIO file +- --- H2_R_0.74.out # pySCF output +--- 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 +--- 02_vmc/ # VMC_Workflow: Jastrow + MO optimization +- --- hamiltonian_data_opt_step_1.h5 +- --- ... +- --- hamiltonian_data_opt_step_20.h5 +--- 03_mcmc/ # MCMC_Workflow: production sampling + forces +--- 04_lrdmc/ # LRDMC_Workflow: LRDMC (a=0.2) + forces ``` ## Workflow DAG @@ -146,8 +146,8 @@ R_0.74/ For each R, the dependency graph is: ``` -pySCF --> WF --> VMC ─┬─--> MCMC - └─--> LRDMC (a=0.2) +pySCF --> WF --> VMC -----> MCMC + ----> LRDMC (a=0.2) ``` All 20 R values are independent and execute in parallel via `Launcher`. @@ -194,7 +194,7 @@ The script prints a summary table after all calculations complete: ## Results -![H2 PES — MCMC and LRDMC](H2_PES_mcmc_lrdmc.png) +![H2 PES -- MCMC and LRDMC](H2_PES_mcmc_lrdmc.png) ## References diff --git a/examples/jqmc-workflow-example02/README.md b/examples/jqmc-workflow-example02/README.md index 1492a8de..30ef3cb4 100644 --- a/examples/jqmc-workflow-example02/README.md +++ b/examples/jqmc-workflow-example02/README.md @@ -149,35 +149,35 @@ After running the pipeline: ``` jqmc-workflow-example02/ -├── run_pipelines.py # Main script -├── run_pyscf.py # Standalone pySCF script (reference) -├── water_trexio.hdf5 # TREXIO file (pySCF output) -├── jqmc_setting_local/ # Machine configuration -├── 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 -├── 02_vmc/ # VMC_Workflow: Jastrow optimization -├── 03_mcmc/ # MCMC production (per walker count) -│ ├── w00008/ -│ ├── w00016/ -│ ├── ... -│ └── w08192/ -├── 04_lrdmc/ # LRDMC production (per walker count) -│ ├── w00008/ -│ ├── w00016/ -│ ├── ... -│ └── w08192/ +--- run_pipelines.py # Main script +--- run_pyscf.py # Standalone pySCF script (reference) +--- water_trexio.hdf5 # TREXIO file (pySCF output) +--- jqmc_setting_local/ # Machine configuration +--- 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 +--- 02_vmc/ # VMC_Workflow: Jastrow optimization +--- 03_mcmc/ # MCMC production (per walker count) +- --- w00008/ +- --- w00016/ +- --- ... +- --- w08192/ +--- 04_lrdmc/ # LRDMC production (per walker count) +- --- w00008/ +- --- w00016/ +- --- ... +- --- w08192/ ``` ## Workflow DAG ``` -pySCF --> WF --> VMC ─┬─--> MCMC (w8) ─┐ - ├─--> MCMC (w16) │ - ├─--> ... ├─--> Summary table - ├─--> MCMC (w8192) │ - ├─--> LRDMC (w8) │ - ├─--> LRDMC (w16) │ - ├─--> ... │ - └─--> LRDMC (w8192) ─┘ +pySCF --> WF --> VMC -----> MCMC (w8) -- + ----> MCMC (w16) - + ----> ... ----> Summary table + ----> MCMC (w8192) - + ----> LRDMC (w8) - + ----> LRDMC (w16) - + ----> ... - + ----> LRDMC (w8192) -- ``` ## Machine configuration @@ -186,8 +186,8 @@ This example assumes a cluster where each node has 4 NVIDIA GPUs. The benchmark | Component | Specification | |-----------|---------------| -| CPU | Intel Xeon Platinum 8490H (Sapphire Rapids, 60 cores, 1.90–3.50 GHz) × 2 sockets | -| GPU | NVIDIA H100 (Hopper) × 4 sockets | +| CPU | Intel Xeon Platinum 8490H (Sapphire Rapids, 60 cores, 1.90-3.50 GHz) x 2 sockets | +| GPU | NVIDIA H100 (Hopper) x 4 sockets | To run on a different cluster, change `SERVER`, `QUEUE_LABEL_s`, and `QUEUE_LABEL_l` in `run_pipelines.py` and provide the appropriate machine configuration in `jqmc_setting_local/`. diff --git a/examples/jqmc-workflow-example03/README.md b/examples/jqmc-workflow-example03/README.md index 68fe20b1..acd845c5 100644 --- a/examples/jqmc-workflow-example03/README.md +++ b/examples/jqmc-workflow-example03/README.md @@ -152,21 +152,21 @@ After running the pipeline: ``` jqmc-workflow-example03/ -├── run_pipelines.py # Main script -├── 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 -├── 02_vmc/ # VMC_Workflow: Jastrow optimization (100 steps) -│ ├── hamiltonian_data_opt_step_1.h5 -│ ├── ... -│ └── hamiltonian_data_opt_step_100.h5 -├── 03_mcmc/ # MCMC_Workflow: production sampling + forces -└── 04_lrdmc/ # LRDMC_Workflow: LRDMC (a=0.30) + forces +--- run_pipelines.py # Main script +--- 01_wf/ # WF_Workflow: TREXIO --> hamiltonian_data.h5 +--- 02_vmc/ # VMC_Workflow: Jastrow optimization (100 steps) +- --- hamiltonian_data_opt_step_1.h5 +- --- ... +- --- hamiltonian_data_opt_step_100.h5 +--- 03_mcmc/ # MCMC_Workflow: production sampling + forces +--- 04_lrdmc/ # LRDMC_Workflow: LRDMC (a=0.30) + forces ``` ## Workflow DAG ``` -pySCF --> WF --> VMC ─┬─--> MCMC (energy + force) - └─--> LRDMC (energy + force) +pySCF --> WF --> VMC -----> MCMC (energy + force) + ----> LRDMC (energy + force) ``` ## Machine configuration diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index f9462f80..6d9d1b3c 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -692,7 +692,7 @@ def _compute_ecp_non_local_parts_all_pairs_debug( """ # Forward r_up/dn_carts/RT as-is (Principle 3a -- no parameter rebind). # Cast RT to coulomb zone at the use site (the grid_points rotation below). - dtype_np = get_dtype_np("coulomb") # noqa: F841 + dtype_np = get_dtype_np("coulomb") if Nv == 4: weights = tetrahedron_sym_mesh_Nv4.weights @@ -880,7 +880,7 @@ def _compute_ecp_non_local_parts_nearest_neighbors_debug( """ # Forward r_up/dn_carts/RT as-is (Principle 3a -- no parameter rebind). # Cast RT to coulomb zone at the use site (the grid_points rotation below). - dtype_np = get_dtype_np("coulomb") # noqa: F841 + dtype_np = get_dtype_np("coulomb") if Nv == 4: weights = tetrahedron_sym_mesh_Nv4.weights diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 2575a2e9..e7b4c906 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -52,7 +52,7 @@ from jax import grad, jit, lax, vmap from jax import numpy as jnp from jax import typing as jnpt -from jax.scipy import linalg as jsp_linalg # noqa: F401 (kept for external callers) +from jax.scipy import linalg as jsp_linalg from mpi4py import MPI from ._diff_mask import DiffMask, apply_diff_mask diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index 08d7b447..53da1415 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -55,7 +55,7 @@ import toml from jax import grad, jit, lax, vmap from jax import numpy as jnp -from jax.scipy.linalg import lu_factor, lu_solve # noqa: F401 (kept for external callers / _MCMC_debug) +from jax.scipy.linalg import lu_factor, lu_solve from mpi4py import MPI from ._diff_mask import DiffMask, apply_diff_mask diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index ed5607fe..b10c7825 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -57,7 +57,7 @@ from flax import struct from jax import jit -from ._jqmc_utility import _cart_to_spherical_matrix, _spherical_to_cart_matrix # noqa: F401 +from ._jqmc_utility import _cart_to_spherical_matrix, _spherical_to_cart_matrix # myqmc module from ._precision import get_dtype_jnp diff --git a/pyproject.toml b/pyproject.toml index 9f3f7100..00c145e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,9 +60,9 @@ lint.extend-ignore = [ "PT006", "PT012", "PT018", # flake8-return "RET501", "RET503", "RET504", "RET505", "RET506", "RET507", "RET508", - # ruff-specific (RUF001/002/003 are ENFORCED — never add here) + # ruff-specific (RUF001/002/003 are ENFORCED -- never add here) "RUF005", "RUF010", "RUF019", "RUF021", "RUF022", "RUF023", - "RUF046", "RUF059", "RUF100", + "RUF046", "RUF059", # flake8-simplify "SIM101", "SIM102", "SIM103", "SIM105", "SIM114", "SIM115", "SIM118", "SIM300", # flake8-print diff --git a/tests/test_AOs.py b/tests/test_AOs.py index 0a3ee74a..292dd338 100755 --- a/tests/test_AOs.py +++ b/tests/test_AOs.py @@ -50,7 +50,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc.atomic_orbital import ( # noqa: E402 +from jqmc.atomic_orbital import ( AOs_cart_data, AOs_sphe_data, _compute_AOs_cart, @@ -70,9 +70,9 @@ compute_AOs_value_grad_lap, compute_overlap_matrix, ) -from jqmc._precision import get_dtype_jnp, get_tolerance, get_tolerance_min # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.structure import Structure_data # noqa: E402 +from jqmc._precision import get_dtype_jnp, get_tolerance, get_tolerance_min +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.structure import Structure_data # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_MOs.py b/tests/test_MOs.py index c56f00c8..60453ac3 100755 --- a/tests/test_MOs.py +++ b/tests/test_MOs.py @@ -47,11 +47,11 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc.atomic_orbital import ( # noqa: E402 +from jqmc.atomic_orbital import ( AOs_cart_data, AOs_sphe_data, ) -from jqmc.molecular_orbital import ( # noqa: E402 +from jqmc.molecular_orbital import ( MOs_data, _cart_to_spherical_matrix, _compute_MOs_debug, @@ -64,9 +64,9 @@ compute_MOs_laplacian, compute_MOs_value_grad_lap, ) -from jqmc._precision import get_dtype_jnp, get_tolerance, get_tolerance_min # noqa: E402 -from jqmc.structure import Structure_data # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 +from jqmc._precision import get_dtype_jnp, get_tolerance, get_tolerance_min +from jqmc.structure import Structure_data +from jqmc.trexio_wrapper import read_trexio_file # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_ao_basis_optimization.py b/tests/test_ao_basis_optimization.py index d106b226..72d1d102 100644 --- a/tests/test_ao_basis_optimization.py +++ b/tests/test_ao_basis_optimization.py @@ -14,17 +14,17 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc.atomic_orbital import AOs_cart_data, AOs_sphe_data, ShellPrimMap # noqa: E402 -from jqmc.determinant import Geminal_data, compute_det_geminal_all_elements # noqa: E402 -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc.atomic_orbital import AOs_cart_data, AOs_sphe_data, ShellPrimMap +from jqmc.determinant import Geminal_data, compute_det_geminal_all_elements +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_three_body_data, compute_Jastrow_three_body, ) -from jqmc._precision import get_tolerance # noqa: E402 -from jqmc.molecular_orbital import MOs_data # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import ( # noqa: E402 +from jqmc._precision import get_tolerance +from jqmc.molecular_orbital import MOs_data +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import ( VariationalParameterBlock, Wavefunction_data, evaluate_ln_wavefunction, diff --git a/tests/test_comparison_with_turborvb_AE.py b/tests/test_comparison_with_turborvb_AE.py index 1c5b426f..c3964e33 100755 --- a/tests/test_comparison_with_turborvb_AE.py +++ b/tests/test_comparison_with_turborvb_AE.py @@ -48,11 +48,11 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc.coulomb_potential import _compute_bare_coulomb_potential_debug, compute_bare_coulomb_potential # noqa: E402 -from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 -from jqmc.jastrow_factor import Jastrow_data # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import Wavefunction_data, compute_kinetic_energy, evaluate_wavefunction # noqa: E402 +from jqmc.coulomb_potential import _compute_bare_coulomb_potential_debug, compute_bare_coulomb_potential +from jqmc.hamiltonians import Hamiltonian_data +from jqmc.jastrow_factor import Jastrow_data +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import Wavefunction_data, compute_kinetic_energy, evaluate_wavefunction # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_comparison_with_turborvb_ECP.py b/tests/test_comparison_with_turborvb_ECP.py index d79cc35e..9f843b8a 100755 --- a/tests/test_comparison_with_turborvb_ECP.py +++ b/tests/test_comparison_with_turborvb_ECP.py @@ -49,18 +49,18 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc.coulomb_potential import ( # noqa: E402 +from jqmc.coulomb_potential import ( _compute_bare_coulomb_potential_debug, _compute_ecp_coulomb_potential_debug, compute_bare_coulomb_potential, compute_ecp_coulomb_potential, ) -from jqmc.determinant import compute_geminal_all_elements # noqa: E402 -from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 -from jqmc.jastrow_factor import Jastrow_data, Jastrow_two_body_data # noqa: E402 -from jqmc.structure import _find_nearest_index_jnp # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import Wavefunction_data, compute_kinetic_energy, evaluate_wavefunction # noqa: E402 +from jqmc.determinant import compute_geminal_all_elements +from jqmc.hamiltonians import Hamiltonian_data +from jqmc.jastrow_factor import Jastrow_data, Jastrow_two_body_data +from jqmc.structure import _find_nearest_index_jnp +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import Wavefunction_data, compute_kinetic_energy, evaluate_wavefunction # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_determinant.py b/tests/test_determinant.py index 2ddd11eb..549898e0 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -46,9 +46,9 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 -from jqmc.atomic_orbital import AOs_sphe_data, compute_overlap_matrix # noqa: E402 -from jqmc.determinant import ( # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc.atomic_orbital import AOs_sphe_data, compute_overlap_matrix +from jqmc.determinant import ( Geminal_data, _advance_grads_laplacian_ln_Det_streaming_state, _compute_AS_regularization_factor_debug, @@ -71,10 +71,10 @@ compute_ln_det_geminal_all_elements, compute_ln_det_geminal_all_elements_fast, ) -from jqmc.molecular_orbital import MOs_data # noqa: E402 -from jqmc.structure import Structure_data # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import VariationalParameterBlock # noqa: E402 +from jqmc.molecular_orbital import MOs_data +from jqmc.structure import Structure_data +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import VariationalParameterBlock # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_ecps.py b/tests/test_ecps.py index c546aaee..ad8f0906 100755 --- a/tests/test_ecps.py +++ b/tests/test_ecps.py @@ -45,8 +45,8 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance # noqa: E402 -from jqmc.coulomb_potential import ( # noqa: E402 +from jqmc._precision import get_tolerance +from jqmc.coulomb_potential import ( _compute_bare_coulomb_potential_debug, _compute_bare_coulomb_potential_el_ion_element_wise_debug, _compute_discretized_bare_coulomb_potential_el_ion_element_wise_debug, @@ -65,15 +65,15 @@ compute_ecp_non_local_parts_nearest_neighbors, compute_ecp_non_local_parts_nearest_neighbors_fast_update, ) -from jqmc.determinant import compute_geminal_all_elements # noqa: E402 -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc.determinant import compute_geminal_all_elements +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_one_body_data, Jastrow_three_body_data, Jastrow_two_body_data, ) -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import Wavefunction_data # noqa: E402 +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import Wavefunction_data # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_hamiltonian.py b/tests/test_hamiltonian.py index 6a0a8883..1fbc92d3 100644 --- a/tests/test_hamiltonian.py +++ b/tests/test_hamiltonian.py @@ -45,26 +45,26 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min from jqmc.determinant import ( - Geminal_data, # noqa: E402 - compute_geminal_all_elements, # noqa: E402 + Geminal_data, + compute_geminal_all_elements, ) -from jqmc.hamiltonians import ( # noqa: E402 +from jqmc.hamiltonians import ( Hamiltonian_data, _compute_local_energy_auto, compute_local_energy, compute_local_energy_fast, ) -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_NN_data, Jastrow_one_body_data, Jastrow_three_body_data, Jastrow_two_body_data, ) -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import ( # noqa: E402 +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import ( Wavefunction_data, ) diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index bf969b5f..072c1edb 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -43,9 +43,9 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 -from jqmc.atomic_orbital import AOs_sphe_data # noqa: E402 -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc.atomic_orbital import AOs_sphe_data +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_NN_data, Jastrow_one_body_data, @@ -72,9 +72,9 @@ compute_Jastrow_three_body, compute_Jastrow_two_body, ) -from jqmc.molecular_orbital import MOs_data # noqa: E402 -from jqmc.structure import Structure_data # noqa: E402 -from jqmc.wavefunction import VariationalParameterBlock # noqa: E402 +from jqmc.molecular_orbital import MOs_data +from jqmc.structure import Structure_data +from jqmc.wavefunction import VariationalParameterBlock @pytest.mark.parametrize("j1b_type", ["exp", "pade"]) diff --git a/tests/test_jqmc_command_lines.py b/tests/test_jqmc_command_lines.py index cee89990..dd48f415 100644 --- a/tests/test_jqmc_command_lines.py +++ b/tests/test_jqmc_command_lines.py @@ -43,8 +43,8 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc.jqmc_cli import _cli # noqa: E402 -from jqmc.jqmc_tool import ( # noqa: E402 +from jqmc.jqmc_cli import _cli +from jqmc.jqmc_tool import ( lrdmc_compute_energy, lrdmc_extrapolate_energy, lrdmc_generate_input, diff --git a/tests/test_jqmc_gfmc_bra.py b/tests/test_jqmc_gfmc_bra.py index 071bedd7..f32f03d8 100755 --- a/tests/test_jqmc_gfmc_bra.py +++ b/tests/test_jqmc_gfmc_bra.py @@ -45,18 +45,18 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 -from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc.hamiltonians import Hamiltonian_data +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_NN_data, Jastrow_one_body_data, Jastrow_three_body_data, Jastrow_two_body_data, ) -from jqmc.jqmc_gfmc import GFMC_n, _GFMC_n_debug # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import Wavefunction_data # noqa: E402 +from jqmc.jqmc_gfmc import GFMC_n, _GFMC_n_debug +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import Wavefunction_data # MPI related mpi_comm = MPI.COMM_WORLD diff --git a/tests/test_jqmc_gfmc_tau.py b/tests/test_jqmc_gfmc_tau.py index 347b63d5..bcc788ab 100755 --- a/tests/test_jqmc_gfmc_tau.py +++ b/tests/test_jqmc_gfmc_tau.py @@ -45,18 +45,18 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 -from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc.hamiltonians import Hamiltonian_data +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_NN_data, Jastrow_one_body_data, Jastrow_three_body_data, Jastrow_two_body_data, ) -from jqmc.jqmc_gfmc import GFMC_t, _GFMC_t_debug # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import Wavefunction_data # noqa: E402 +from jqmc.jqmc_gfmc import GFMC_t, _GFMC_t_debug +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import Wavefunction_data # MPI related mpi_comm = MPI.COMM_WORLD diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index 968ad0a3..f539c344 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -47,19 +47,19 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 -from jqmc.determinant import Geminal_data # noqa: E402 -from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc.determinant import Geminal_data +from jqmc.hamiltonians import Hamiltonian_data +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_NN_data, Jastrow_one_body_data, Jastrow_three_body_data, Jastrow_two_body_data, ) -from jqmc.jqmc_mcmc import MCMC, _MCMC_debug # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import VariationalParameterBlock, Wavefunction_data, evaluate_ln_wavefunction # noqa: E402 +from jqmc.jqmc_mcmc import MCMC, _MCMC_debug +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import VariationalParameterBlock, Wavefunction_data, evaluate_ln_wavefunction # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_jqmc_tool.py b/tests/test_jqmc_tool.py index ea13ea4c..64407d87 100644 --- a/tests/test_jqmc_tool.py +++ b/tests/test_jqmc_tool.py @@ -49,7 +49,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc.jqmc_tool import ( # noqa: E402 +from jqmc.jqmc_tool import ( _J3_PERIOD_RANGES, hamiltonian_show_info, hamiltonian_to_xyz, @@ -61,8 +61,8 @@ vmc_analyze_output, vmc_generate_input, ) -from jqmc._precision import get_tolerance # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 +from jqmc._precision import get_tolerance +from jqmc.trexio_wrapper import read_trexio_file trexio_files = [ "H2_ecp_ccpvtz_cart.h5", diff --git a/tests/test_lrdmc_force.py b/tests/test_lrdmc_force.py index 337d8ef1..ac413cdc 100755 --- a/tests/test_lrdmc_force.py +++ b/tests/test_lrdmc_force.py @@ -44,9 +44,9 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance_min # noqa: E402 -from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc._precision import get_tolerance_min +from jqmc.hamiltonians import Hamiltonian_data +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_NN_data, Jastrow_one_body_data, @@ -54,11 +54,11 @@ Jastrow_two_body_data, ) from jqmc.jqmc_gfmc import ( - GFMC_n, # noqa: E402 - GFMC_t, # noqa: E402 + GFMC_n, + GFMC_t, ) -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import Wavefunction_data # noqa: E402 +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import Wavefunction_data # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_mcmc_force.py b/tests/test_mcmc_force.py index 07addaef..23cbd4d9 100755 --- a/tests/test_mcmc_force.py +++ b/tests/test_mcmc_force.py @@ -44,18 +44,18 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance_min # noqa: E402 -from jqmc.hamiltonians import Hamiltonian_data # noqa: E402 -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc._precision import get_tolerance_min +from jqmc.hamiltonians import Hamiltonian_data +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_NN_data, Jastrow_one_body_data, Jastrow_three_body_data, Jastrow_two_body_data, ) -from jqmc.jqmc_mcmc import MCMC # noqa: E402 -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import Wavefunction_data # noqa: E402 +from jqmc.jqmc_mcmc import MCMC +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import Wavefunction_data # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_mixed_precision.py b/tests/test_mixed_precision.py index 3f9b1968..347cb81a 100644 --- a/tests/test_mixed_precision.py +++ b/tests/test_mixed_precision.py @@ -31,28 +31,28 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import configure, get_dtype_jnp # noqa: E402 -from jqmc.atomic_orbital import ( # noqa: E402 +from jqmc._precision import configure, get_dtype_jnp +from jqmc.atomic_orbital import ( compute_AOs, compute_AOs_grad, compute_AOs_laplacian, ) -from jqmc.coulomb_potential import ( # noqa: E402 +from jqmc.coulomb_potential import ( compute_bare_coulomb_potential, compute_bare_coulomb_potential_el_el, compute_bare_coulomb_potential_el_ion_element_wise, compute_ecp_local_parts_all_pairs, compute_ecp_non_local_part_all_pairs_jax_weights_grid_points, ) -from jqmc.determinant import ( # noqa: E402 +from jqmc.determinant import ( compute_geminal_all_elements, compute_geminal_dn_one_column_elements, compute_geminal_up_one_row_elements, compute_grads_and_laplacian_ln_Det, compute_ln_det_geminal_all_elements, ) -from jqmc.hamiltonians import Hamiltonian_data, compute_local_energy # noqa: E402 -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc.hamiltonians import Hamiltonian_data, compute_local_energy +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_one_body_data, Jastrow_three_body_data, @@ -62,13 +62,13 @@ compute_Jastrow_three_body, compute_Jastrow_two_body, ) -from jqmc.molecular_orbital import ( # noqa: E402 +from jqmc.molecular_orbital import ( compute_MOs, compute_MOs_grad, compute_MOs_laplacian, ) -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import ( # noqa: E402 +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import ( Wavefunction_data, compute_kinetic_energy, evaluate_ln_wavefunction, diff --git a/tests/test_swct.py b/tests/test_swct.py index 17b4d693..f544d99d 100755 --- a/tests/test_swct.py +++ b/tests/test_swct.py @@ -44,14 +44,14 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance # noqa: E402 -from jqmc.swct import ( # noqa: E402 +from jqmc._precision import get_tolerance +from jqmc.swct import ( _evaluate_swct_domega_debug, _evaluate_swct_omega_debug, evaluate_swct_domega, evaluate_swct_omega, ) -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 +from jqmc.trexio_wrapper import read_trexio_file # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_trexio.py b/tests/test_trexio.py index b2c761a7..9f77ba07 100755 --- a/tests/test_trexio.py +++ b/tests/test_trexio.py @@ -43,7 +43,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 +from jqmc.trexio_wrapper import read_trexio_file # JAX float64 jax.config.update("jax_enable_x64", True) diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index c9eac251..11b166be 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -45,16 +45,16 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance, get_tolerance_min # noqa: E402 -from jqmc.determinant import compute_geminal_all_elements # noqa: E402 -from jqmc.jastrow_factor import ( # noqa: E402 +from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc.determinant import compute_geminal_all_elements +from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_NN_data, Jastrow_three_body_data, Jastrow_two_body_data, ) -from jqmc.trexio_wrapper import read_trexio_file # noqa: E402 -from jqmc.wavefunction import ( # noqa: E402 +from jqmc.trexio_wrapper import read_trexio_file +from jqmc.wavefunction import ( Wavefunction_data, _advance_kinetic_energy_all_elements_streaming_state, _compute_discretized_kinetic_energy_debug, From 72ba1836f6369ff0a2d9aebbac6a91bc28dbbb73 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 01:30:42 +0900 Subject: [PATCH 47/97] ruff: apply the second autofix (only safefix) --- benchmarks/benchmark_kinetic_energy.py | 8 +- benchmarks/benchmark_ratio_determinant.py | 2 +- benchmarks/benchmark_ratio_jastrow.py | 2 +- examples/sync_examples.py | 4 +- jqmc/_diff_mask.py | 5 +- jqmc/_jqmc_utility.py | 4 +- jqmc/atomic_orbital.py | 8 +- jqmc/hamiltonians.py | 4 +- jqmc/jastrow_factor.py | 3 +- jqmc/jqmc_cli.py | 2 +- jqmc/jqmc_tool.py | 8 +- jqmc_workflow/_cli.py | 2 +- jqmc_workflow/_error_estimator.py | 2 +- jqmc_workflow/_job.py | 2 +- jqmc_workflow/_lrdmc_calibration.py | 8 +- jqmc_workflow/_machine.py | 6 +- jqmc_workflow/_output_parser.py | 12 +-- jqmc_workflow/_results.py | 98 +++++++++---------- jqmc_workflow/_state.py | 2 +- jqmc_workflow/_transfer.py | 2 +- jqmc_workflow/launcher.py | 2 +- jqmc_workflow/lrdmc_ext_workflow.py | 31 +++--- jqmc_workflow/lrdmc_workflow.py | 32 +++--- jqmc_workflow/mcmc_workflow.py | 28 +++--- jqmc_workflow/vmc_workflow.py | 58 +++++------ jqmc_workflow/wf_workflow.py | 16 +-- jqmc_workflow/workflow.py | 24 ++--- pyproject.toml | 3 +- tests/test_jqmc_command_lines.py | 12 +-- .../read_jastrow_factor_from_turbo_wf.py | 2 +- tools/read_Jastrow_factor_from_turbo_wf.py | 2 +- 31 files changed, 197 insertions(+), 197 deletions(-) diff --git a/benchmarks/benchmark_kinetic_energy.py b/benchmarks/benchmark_kinetic_energy.py index bc34d64e..9324e92c 100644 --- a/benchmarks/benchmark_kinetic_energy.py +++ b/benchmarks/benchmark_kinetic_energy.py @@ -119,8 +119,8 @@ leaf.block_until_ready() all_elements_auto_times.append(time.perf_counter() - start) -print(" all_elements analytic : {:.6f}".format(np.mean(all_elements_analytic_times))) -print(" all_elements autodiff : {:.6f}".format(np.mean(all_elements_auto_times))) +print(f" all_elements analytic : {np.mean(all_elements_analytic_times):.6f}") +print(f" all_elements autodiff : {np.mean(all_elements_auto_times):.6f}") if BENCH_NN: jastrow_nn_data = Jastrow_NN_data.init_from_structure( @@ -191,5 +191,5 @@ leaf.block_until_ready() all_elements_auto_times.append(time.perf_counter() - start) - print(" all_elements analytic : {:.6f}".format(np.mean(all_elements_analytic_times))) - print(" all_elements autodiff : {:.6f}".format(np.mean(all_elements_auto_times))) + print(f" all_elements analytic : {np.mean(all_elements_analytic_times):.6f}") + print(f" all_elements autodiff : {np.mean(all_elements_auto_times):.6f}") diff --git a/benchmarks/benchmark_ratio_determinant.py b/benchmarks/benchmark_ratio_determinant.py index 0f40b49c..a0af17ad 100644 --- a/benchmarks/benchmark_ratio_determinant.py +++ b/benchmarks/benchmark_ratio_determinant.py @@ -8,7 +8,7 @@ import time from dataclasses import dataclass -from typing import Sequence +from collections.abc import Sequence import jax.numpy as jnp import numpy as np diff --git a/benchmarks/benchmark_ratio_jastrow.py b/benchmarks/benchmark_ratio_jastrow.py index 168c8444..373e5b76 100644 --- a/benchmarks/benchmark_ratio_jastrow.py +++ b/benchmarks/benchmark_ratio_jastrow.py @@ -8,7 +8,7 @@ import time from dataclasses import dataclass -from typing import Sequence +from collections.abc import Sequence import jax.numpy as jnp import numpy as np diff --git a/examples/sync_examples.py b/examples/sync_examples.py index cb28dfbf..90240dab 100644 --- a/examples/sync_examples.py +++ b/examples/sync_examples.py @@ -9,7 +9,7 @@ def sync_readme(readme_path): and updates the following code block with the content of the file. """ try: - with open(readme_path, "r") as f: + with open(readme_path) as f: content = f.read() except FileNotFoundError: print(f"File not found: {readme_path}") @@ -39,7 +39,7 @@ def replace_chunk(match): return match.group(0) # Return original text if file not found try: - with open(source_path, "r") as src: + with open(source_path) as src: new_content = src.read() # Ensure the content ends with exactly one newline if it's not empty new_content = new_content.rstrip() + "\n" diff --git a/jqmc/_diff_mask.py b/jqmc/_diff_mask.py index 97a59ac5..b9a678c2 100644 --- a/jqmc/_diff_mask.py +++ b/jqmc/_diff_mask.py @@ -9,7 +9,8 @@ from __future__ import annotations from dataclasses import dataclass, fields, is_dataclass -from typing import Any, Iterable +from typing import Any +from collections.abc import Iterable import jax @@ -34,7 +35,7 @@ class DiffMask: params: bool = True coords: bool = True - def update(self, *, params: bool | None = None, coords: bool | None = None) -> "DiffMask": + def update(self, *, params: bool | None = None, coords: bool | None = None) -> DiffMask: """Return a new mask with any provided overrides applied.""" return DiffMask( params=self.params if params is None else params, diff --git a/jqmc/_jqmc_utility.py b/jqmc/_jqmc_utility.py index a1276bff..3cccd021 100644 --- a/jqmc/_jqmc_utility.py +++ b/jqmc/_jqmc_utility.py @@ -38,7 +38,7 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from functools import lru_cache +from functools import lru_cache, cache from logging import getLogger import numpy as np @@ -565,7 +565,7 @@ def _generate_init_electron_configurations_debug( return r_carts_up, r_carts_dn, up_owner, dn_owner -@lru_cache(maxsize=None) +@cache def _cart_to_spherical_matrix(l: int) -> np.ndarray: """Precomputed cart -> real-spherical transform for angular momentum ``l`` (0-6). diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index b1d6bb7e..612ebc54 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -411,7 +411,7 @@ def sanity_check(self) -> None: """ self.structure_data.sanity_check() - def _build_uncontracted_aos(self) -> "AOs_cart_data": + def _build_uncontracted_aos(self) -> AOs_cart_data: """Return an uncontracted AO dataset by unique exponents per AO.""" seen = set() new_nucleus_index = [] @@ -1057,7 +1057,7 @@ def sanity_check(self) -> None: self.structure_data.sanity_check() - def _build_uncontracted_aos(self) -> "AOs_sphe_data": + def _build_uncontracted_aos(self) -> AOs_sphe_data: """Return an uncontracted AO dataset by unique exponents per AO.""" seen = set() new_nucleus_index = [] @@ -1405,7 +1405,7 @@ def symmetrize(self, arr: np.ndarray) -> np.ndarray: # ---- constructors ---- @classmethod - def from_aos_data(cls, aos_data: "AOs_sphe_data | AOs_cart_data") -> "ShellPrimMap": + def from_aos_data(cls, aos_data: AOs_sphe_data | AOs_cart_data) -> ShellPrimMap: """Build a shell map from an AO dataclass instance.""" # Build-time copy of fp64 basis-data storage (see _precision.py exemption for # basis-data storage accessors); used only for shell identity and indexing. @@ -1452,7 +1452,7 @@ def from_aos_data(cls, aos_data: "AOs_sphe_data | AOs_cart_data") -> "ShellPrimM return cls(np.array(unique_prim_indices, dtype=np.int32), prim_to_unique) @classmethod - def concat(cls, map_a: "ShellPrimMap", map_b: "ShellPrimMap") -> "ShellPrimMap": + def concat(cls, map_a: ShellPrimMap, map_b: ShellPrimMap) -> ShellPrimMap: """Concatenate two shell maps (e.g. up + dn) into a single map.""" unique_indices = np.concatenate( [ diff --git a/jqmc/hamiltonians.py b/jqmc/hamiltonians.py index 28759cb6..5f629cd4 100644 --- a/jqmc/hamiltonians.py +++ b/jqmc/hamiltonians.py @@ -429,7 +429,7 @@ def _save_dataclass_to_hdf5(group: h5py.Group, obj: Any) -> None: _save_item(group, field.name, value) -def _load_item(item: Union[h5py.Group, h5py.Dataset, Any]) -> Any: +def _load_item(item: h5py.Group | h5py.Dataset | Any) -> Any: """Helper to load an item from HDF5.""" if isinstance(item, h5py.Dataset): val = item[()] @@ -479,7 +479,7 @@ def _load_item(item: Union[h5py.Group, h5py.Dataset, Any]) -> Any: return item -def _load_dataclass_from_hdf5(cls: Type[T], group: h5py.Group) -> T: +def _load_dataclass_from_hdf5(cls: type[T], group: h5py.Group) -> T: """Recursively load a dataclass from an HDF5 group. Args: diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 1ee20de6..46fc8831 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -48,7 +48,8 @@ from logging import getLogger # jqmc module -from typing import TYPE_CHECKING, Any, Sequence +from typing import TYPE_CHECKING, Any +from collections.abc import Sequence # jax modules import jax diff --git a/jqmc/jqmc_cli.py b/jqmc/jqmc_cli.py index 2c17a229..75f1a982 100644 --- a/jqmc/jqmc_cli.py +++ b/jqmc/jqmc_cli.py @@ -100,7 +100,7 @@ def _cli(): if not os.path.isfile(toml_file): raise FileNotFoundError(f"toml_file = {toml_file} does not exist.") else: - with open(toml_file, "r") as f: + with open(toml_file) as f: dict_toml = toml.load(f) # MPI related diff --git a/jqmc/jqmc_tool.py b/jqmc/jqmc_tool.py index d4db338b..24623e59 100644 --- a/jqmc/jqmc_tool.py +++ b/jqmc/jqmc_tool.py @@ -241,7 +241,7 @@ def trexio_convert_to( "--jastrow-nn-type", help="NN Jastrow type (e.g. 'schnet'). If set, an NN-based Jastrow term is added.", ), - j_nn_params: List[str] = typer.Option( + j_nn_params: list[str] = typer.Option( None, "-jp", "--jastrow-nn-param", @@ -657,7 +657,7 @@ def vmc_generate_input( @vmc_app.command("analyze-output") def vmc_analyze_output( - filenames: List[str] = typer.Argument(..., help="Output files of vmc optimizations."), + filenames: list[str] = typer.Argument(..., help="Output files of vmc optimizations."), plot_graph: bool = typer.Option(False, "-p", "--plot_graph", help="Plot a graph summerizing the result using matplotlib."), save_graph: str = typer.Option(None, "-s", "--save-graph", help="Specify a graph filename."), ): @@ -673,7 +673,7 @@ def vmc_analyze_output( signal_to_noise_pattern = re.compile(r"Max of signal-to-noise of f = max\(\|f\|/\|std f\|\) = ([-+]?\d+(?:\.\d+)?)(?:\.)?") for filename in filenames: - with open(filename, "r") as f: + with open(filename) as f: for line in f: # iter iter_match = iter_pattern.search(line) @@ -1282,7 +1282,7 @@ def lrdmc_compute_force( @lrdmc_app.command("extrapolate-energy") def lrdmc_extrapolate_energy( - restart_chks: List[str] = typer.Argument(..., help="Restart checkpoint files, e.g. lrdmc.rchk"), + restart_chks: list[str] = typer.Argument(..., help="Restart checkpoint files, e.g. lrdmc.rchk"), polynomial_order: int = typer.Option( 2, "-p", diff --git a/jqmc_workflow/_cli.py b/jqmc_workflow/_cli.py index 5257a882..76607084 100644 --- a/jqmc_workflow/_cli.py +++ b/jqmc_workflow/_cli.py @@ -242,7 +242,7 @@ def show_detail(self, job_id: int): logger.info("") logger.info(" workflow_state.toml:") logger.info(" " + "-" * 40) - with open(state_file, "r") as f: + with open(state_file) as f: for line in f: logger.info(f" {line.rstrip()}") logger.info(" " + "-" * 40) diff --git a/jqmc_workflow/_error_estimator.py b/jqmc_workflow/_error_estimator.py index e986838c..f72dc4ed 100644 --- a/jqmc_workflow/_error_estimator.py +++ b/jqmc_workflow/_error_estimator.py @@ -251,7 +251,7 @@ def parse_net_time(output_file: str) -> float | None: return None try: - with open(output_file, "r", errors="replace") as fh: + with open(output_file, errors="replace") as fh: text = fh.read() except OSError as exc: logger.debug(f"parse_net_time: cannot read {output_file}: {exc}") diff --git a/jqmc_workflow/_job.py b/jqmc_workflow/_job.py index d3bb83fb..7db523a1 100644 --- a/jqmc_workflow/_job.py +++ b/jqmc_workflow/_job.py @@ -178,7 +178,7 @@ def generate_script(self, submission_script: str = "submit.sh", *, work_dir=None self.server_machine.name, self.job_submission_template, ) - with open(template_path, "r") as f: + with open(template_path) as f: lines = f.readlines() def replace_kw(lines, keyword, value): diff --git a/jqmc_workflow/_lrdmc_calibration.py b/jqmc_workflow/_lrdmc_calibration.py index 34a849b7..70fdaf03 100644 --- a/jqmc_workflow/_lrdmc_calibration.py +++ b/jqmc_workflow/_lrdmc_calibration.py @@ -92,7 +92,7 @@ def get_num_electrons(hamiltonian_file: str) -> int: _SURVIVED_PATTERN = re.compile(r"Survived walkers ratio\s*=\s*(\d+\.?\d*)\s*%") -def parse_survived_walkers_ratio(output_file: str) -> Optional[float]: +def parse_survived_walkers_ratio(output_file: str) -> float | None: """Parse the survived walkers ratio from an LRDMC output file. Searches for the line @@ -111,7 +111,7 @@ def parse_survived_walkers_ratio(output_file: str) -> Optional[float]: """ last_value = None try: - with open(output_file, "r") as f: + with open(output_file) as f: for line in f: m = _SURVIVED_PATTERN.search(line) if m: @@ -125,8 +125,8 @@ def parse_survived_walkers_ratio(output_file: str) -> Optional[float]: def fit_num_projection_per_measurement( - x_values: List[int], - y_values: List[float], + x_values: list[int], + y_values: list[float], target_ratio: float, ) -> int: r"""Determine the optimal ``num_projection_per_measurement`` by linear fit. diff --git a/jqmc_workflow/_machine.py b/jqmc_workflow/_machine.py index 08f74896..96245be4 100644 --- a/jqmc_workflow/_machine.py +++ b/jqmc_workflow/_machine.py @@ -76,7 +76,7 @@ def __init__(self, machine: str): # Load machine data try: - with open(self.machine_info_yaml, "r") as yf: + with open(self.machine_info_yaml) as yf: self.data = yaml.safe_load(yf)[machine] except FileNotFoundError: logger.error(f"Config file {self.machine_info_yaml} not found!") @@ -159,7 +159,7 @@ def ssh_open(self): ssh_config = paramiko.SSHConfig() config_file = os.path.join(os.getenv("HOME"), ".ssh/config") try: - with open(config_file, "r") as fh: + with open(config_file) as fh: ssh_config.parse(fh) except FileNotFoundError: logger.error(f"SSH config file ({config_file}) is required.") @@ -172,7 +172,7 @@ def ssh_open(self): except KeyError: from io import StringIO - with open(config_file, "r") as fh: + with open(config_file) as fh: original = fh.read() # Disable CanonicalizeHostname to avoid the paramiko bug patched = re.sub( diff --git a/jqmc_workflow/_output_parser.py b/jqmc_workflow/_output_parser.py index 29f69256..36692b0c 100644 --- a/jqmc_workflow/_output_parser.py +++ b/jqmc_workflow/_output_parser.py @@ -207,7 +207,7 @@ def repair_forces_from_output(work_dir: str) -> bool: # Read and parse the force table from the output file try: - with open(last_out, "r") as f: + with open(last_out) as f: text = f.read() except Exception: return False @@ -364,12 +364,12 @@ def repair_forces_from_output(work_dir: str) -> bool: # -- Internal helpers ---------------------------------------------- -def _read_text(path: str) -> Optional[str]: +def _read_text(path: str) -> str | None: """Read a text file, returning *None* if it doesn't exist or can't be read.""" if not os.path.isfile(path): return None try: - with open(path, "r", errors="replace") as fh: + with open(path, errors="replace") as fh: return fh.read() except OSError as exc: logger.debug("Cannot read %s: %s", path, exc) @@ -406,7 +406,7 @@ def _find_input_files(work_dir: str) -> list: return [path for _, path in files] -def _find_hamiltonian_h5(work_dir: str) -> Optional[str]: +def _find_hamiltonian_h5(work_dir: str) -> str | None: """Extract ``[control] hamiltonian_h5`` from input TOML files in *work_dir*. Uses ``workflow_state.toml`` ``[[jobs]]`` to locate input files. @@ -549,8 +549,8 @@ def _parse_vmc_log_text(text: str) -> list: One entry per optimization step found. """ steps: list[VMC_Step_Data] = [] - current: Optional[VMC_Step_Data] = None - total_opt_steps: Optional[int] = None + current: VMC_Step_Data | None = None + total_opt_steps: int | None = None for line in text.splitlines(): # -- Optimization step header -- diff --git a/jqmc_workflow/_results.py b/jqmc_workflow/_results.py index 111da304..f0d732e8 100644 --- a/jqmc_workflow/_results.py +++ b/jqmc_workflow/_results.py @@ -95,16 +95,16 @@ class VMC_Step_Data: """ step: int - energy: Optional[float] = None - energy_error: Optional[float] = None - max_force: Optional[float] = None - max_force_error: Optional[float] = None - signal_to_noise_ratio: Optional[float] = None - avg_walker_weight: Optional[float] = None - acceptance_ratio: Optional[float] = None - total_time_sec: Optional[float] = None - precompilation_time_sec: Optional[float] = None - net_time_sec: Optional[float] = None + energy: float | None = None + energy_error: float | None = None + max_force: float | None = None + max_force_error: float | None = None + signal_to_noise_ratio: float | None = None + avg_walker_weight: float | None = None + acceptance_ratio: float | None = None + total_time_sec: float | None = None + precompilation_time_sec: float | None = None + net_time_sec: float | None = None timing_breakdown: dict = field(default_factory=dict) @@ -146,15 +146,15 @@ class VMC_Diagnostic_Data: """ steps: list = field(default_factory=list) # list[VMC_Step_Data] - total_opt_steps: Optional[int] = None - total_opt_time_sec: Optional[float] = None + total_opt_steps: int | None = None + total_opt_time_sec: float | None = None opt_timing_breakdown: dict = field(default_factory=dict) - optimized_hamiltonian: Optional[str] = None - restart_checkpoint: Optional[str] = None - num_mpi_processes: Optional[int] = None - num_walkers_per_process: Optional[int] = None - jax_backend: Optional[str] = None - jax_devices: Optional[list] = None + optimized_hamiltonian: str | None = None + restart_checkpoint: str | None = None + num_mpi_processes: int | None = None + num_walkers_per_process: int | None = None + jax_backend: str | None = None + jax_devices: list | None = None stderr_tail: str = "" @@ -205,21 +205,21 @@ class MCMC_Diagnostic_Data: Last portion of stderr (up to 200 lines). """ - acceptance_ratio: Optional[float] = None - avg_walker_weight: Optional[float] = None - total_time_sec: Optional[float] = None - precompilation_time_sec: Optional[float] = None - net_time_sec: Optional[float] = None + acceptance_ratio: float | None = None + avg_walker_weight: float | None = None + total_time_sec: float | None = None + precompilation_time_sec: float | None = None + net_time_sec: float | None = None timing_breakdown: dict = field(default_factory=dict) - energy: Optional[float] = None - energy_error: Optional[float] = None - atomic_forces: Optional[list] = None - hamiltonian_data_file: Optional[str] = None - restart_checkpoint: Optional[str] = None - num_mpi_processes: Optional[int] = None - num_walkers_per_process: Optional[int] = None - jax_backend: Optional[str] = None - jax_devices: Optional[list] = None + energy: float | None = None + energy_error: float | None = None + atomic_forces: list | None = None + hamiltonian_data_file: str | None = None + restart_checkpoint: str | None = None + num_mpi_processes: int | None = None + num_walkers_per_process: int | None = None + jax_backend: str | None = None + jax_devices: list | None = None stderr_tail: str = "" @@ -271,21 +271,21 @@ class LRDMC_Diagnostic_Data: Last portion of stderr (up to 200 lines). """ - survived_walkers_ratio: Optional[float] = None - avg_num_projections: Optional[float] = None - total_time_sec: Optional[float] = None - precompilation_time_sec: Optional[float] = None - net_time_sec: Optional[float] = None + survived_walkers_ratio: float | None = None + avg_num_projections: float | None = None + total_time_sec: float | None = None + precompilation_time_sec: float | None = None + net_time_sec: float | None = None timing_breakdown: dict = field(default_factory=dict) - energy: Optional[float] = None - energy_error: Optional[float] = None - atomic_forces: Optional[list] = None - hamiltonian_data_file: Optional[str] = None - restart_checkpoint: Optional[str] = None - num_mpi_processes: Optional[int] = None - num_walkers_per_process: Optional[int] = None - jax_backend: Optional[str] = None - jax_devices: Optional[list] = None + energy: float | None = None + energy_error: float | None = None + atomic_forces: list | None = None + hamiltonian_data_file: str | None = None + restart_checkpoint: str | None = None + num_mpi_processes: int | None = None + num_walkers_per_process: int | None = None + jax_backend: str | None = None + jax_devices: list | None = None stderr_tail: str = "" @@ -308,8 +308,8 @@ class LRDMC_Ext_Diagnostic_Data: Last portion of stderr (up to 200 lines). """ - extrapolated_energy: Optional[float] = None - extrapolated_energy_error: Optional[float] = None + extrapolated_energy: float | None = None + extrapolated_energy_error: float | None = None per_alat_results: list = field(default_factory=list) stderr_tail: str = "" @@ -346,5 +346,5 @@ class Input_Parameters: in ``workflow_state.toml``. """ - actual_opt_steps: Optional[int] = None + actual_opt_steps: int | None = None per_input: list = field(default_factory=list) # list[dict] diff --git a/jqmc_workflow/_state.py b/jqmc_workflow/_state.py index 80ddbfbf..4e633248 100644 --- a/jqmc_workflow/_state.py +++ b/jqmc_workflow/_state.py @@ -184,7 +184,7 @@ def _check_normal_termination(directory: str, jobs: list) -> list[str]: if not os.path.isfile(filepath): continue # not fetched yet -- nothing to check try: - with open(filepath, "r", errors="replace") as f: + with open(filepath, errors="replace") as f: # Read only the tail (last 8 KiB) for efficiency; # "Program ends ..." is always the last log line. f.seek(0, 2) diff --git a/jqmc_workflow/_transfer.py b/jqmc_workflow/_transfer.py index 4f31b8e4..c7a90ea9 100644 --- a/jqmc_workflow/_transfer.py +++ b/jqmc_workflow/_transfer.py @@ -212,7 +212,7 @@ def get_objects(self, from_objects=None, exclude_patterns=None, *, work_dir=None self.server_machine.ssh_open() try: entries = self.server_machine.sftp.listdir(server_dir) - except IOError: + except OSError: entries = [] matched = [e for e in entries if fnmatch.fnmatch(e, pattern)] expanded.extend(matched) diff --git a/jqmc_workflow/launcher.py b/jqmc_workflow/launcher.py index 05b52445..20e88411 100644 --- a/jqmc_workflow/launcher.py +++ b/jqmc_workflow/launcher.py @@ -150,7 +150,7 @@ class Launcher: def __init__( self, - workflows: Optional[List[Container]] = None, + workflows: list[Container] | None = None, log_level: str = "INFO", log_name: str = "jqmc_workflow.log", draw_graph: bool = False, diff --git a/jqmc_workflow/lrdmc_ext_workflow.py b/jqmc_workflow/lrdmc_ext_workflow.py index d0862181..2c323e1b 100644 --- a/jqmc_workflow/lrdmc_ext_workflow.py +++ b/jqmc_workflow/lrdmc_ext_workflow.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """LRDMC_Ext_Workflow -- LRDMC extrapolation to the a^2->0 limit. Orchestrates multiple :class:`LRDMC_Workflow` runs at different lattice @@ -232,10 +231,10 @@ class LRDMC_Ext_Workflow(Workflow): def __init__( self, server_machine_name: str = "localhost", - alat_list: Optional[List[float]] = None, + alat_list: list[float] | None = None, hamiltonian_file: str = "hamiltonian_data.h5", queue_label: str = "default", - pilot_queue_label: Optional[str] = None, + pilot_queue_label: str | None = None, jobname_prefix: str = "jqmc-lrdmc", number_of_walkers: int = 4, max_time: int = 86400, @@ -244,24 +243,24 @@ def __init__( num_gfmc_warmup_steps: int = 0, num_gfmc_collect_steps: int = 5, # -- [lrdmc-bra / lrdmc-tau] section parameters -- - time_projection_tau: Optional[float] = 0.10, - target_survived_walkers_ratio: Optional[float] = None, - num_projection_per_measurement: Optional[Union[int, Dict[float, int]]] = None, - non_local_move: Optional[str] = None, - E_scf: Optional[float] = None, - atomic_force: Optional[bool] = None, - use_swct: Optional[bool] = None, - epsilon_PW: Optional[float] = None, + time_projection_tau: float | None = 0.10, + target_survived_walkers_ratio: float | None = None, + num_projection_per_measurement: int | dict[float, int] | None = None, + non_local_move: str | None = None, + E_scf: float | None = None, + atomic_force: bool | None = None, + use_swct: bool | None = None, + epsilon_PW: float | None = None, # -- [control] section parameters -- - mcmc_seed: Optional[int] = None, - verbosity: Optional[str] = None, + mcmc_seed: int | None = None, + verbosity: str | None = None, # -- workflow parameters -- poll_interval: int = 60, target_error: float = 0.001, pilot_steps: int = 100, - num_gfmc_projections: Optional[int] = None, + num_gfmc_projections: int | None = None, max_continuation: int = 5, - cleanup_patterns: Optional[list] = None, + cleanup_patterns: list | None = None, # -- [precision] section -- precision_mode: str = "full", ): @@ -489,7 +488,7 @@ async def _run_one(enc): self.status = WorkflowStatus.COMPLETED return self.status, self.output_files, self.output_values - def _extrapolate_energy(self, restart_chks: List[str]): + def _extrapolate_energy(self, restart_chks: list[str]): """Run ``jqmc-tool lrdmc extrapolate-energy``. Returns diff --git a/jqmc_workflow/lrdmc_workflow.py b/jqmc_workflow/lrdmc_workflow.py index c66d5d8b..0ebb5f97 100644 --- a/jqmc_workflow/lrdmc_workflow.py +++ b/jqmc_workflow/lrdmc_workflow.py @@ -305,25 +305,25 @@ def __init__( num_gfmc_warmup_steps: int = 0, num_gfmc_collect_steps: int = 5, # -- [lrdmc-bra / lrdmc-tau] section parameters -- - time_projection_tau: Optional[float] = 0.10, - target_survived_walkers_ratio: Optional[float] = None, - num_projection_per_measurement: Optional[int] = None, - non_local_move: Optional[str] = None, - E_scf: Optional[float] = None, - atomic_force: Optional[bool] = None, - use_swct: Optional[bool] = None, - epsilon_PW: Optional[float] = None, + time_projection_tau: float | None = 0.10, + target_survived_walkers_ratio: float | None = None, + num_projection_per_measurement: int | None = None, + non_local_move: str | None = None, + E_scf: float | None = None, + atomic_force: bool | None = None, + use_swct: bool | None = None, + epsilon_PW: float | None = None, # -- [control] section parameters -- - mcmc_seed: Optional[int] = None, - verbosity: Optional[str] = None, + mcmc_seed: int | None = None, + verbosity: str | None = None, # -- workflow parameters -- poll_interval: int = 60, target_error: float = 0.001, pilot_steps: int = 100, - num_gfmc_projections: Optional[int] = None, - pilot_queue_label: Optional[str] = None, + num_gfmc_projections: int | None = None, + pilot_queue_label: str | None = None, max_continuation: int = 1, - cleanup_patterns: Optional[list] = None, + cleanup_patterns: list | None = None, # -- [precision] section -- precision_mode: str = "full", ): @@ -1195,7 +1195,7 @@ async def _launch_auto(self, _wd): # -- Utility methods ------------------------------------------- - def _find_restart_chk(self, work_dir: str) -> Optional[str]: + def _find_restart_chk(self, work_dir: str) -> str | None: """Locate the LRDMC restart checkpoint file in *work_dir*.""" for pattern in ["restart.h5", "lrdmc.h5", "*.h5"]: matches = sorted(glob.glob(os.path.join(work_dir, pattern))) @@ -1203,7 +1203,7 @@ def _find_restart_chk(self, work_dir: str) -> Optional[str]: return os.path.basename(matches[-1]) return None - def _compute_energy(self, restart_chk: str, work_dir: str, output_file: Optional[str] = None): + def _compute_energy(self, restart_chk: str, work_dir: str, output_file: str | None = None): """Parse energy from *output_file* or run ``jqmc-tool lrdmc compute-energy``. When *output_file* is given the energy is read directly from @@ -1274,7 +1274,7 @@ def _parse_energy_output(text: str): return float(match.group(1)), float(match.group(2)) return None, None - def _compute_force(self, restart_chk: str, work_dir: str, output_file: Optional[str] = None): + def _compute_force(self, restart_chk: str, work_dir: str, output_file: str | None = None): """Parse forces from *output_file* or run ``jqmc-tool lrdmc compute-force``. When *output_file* is given, forces are read directly from diff --git a/jqmc_workflow/mcmc_workflow.py b/jqmc_workflow/mcmc_workflow.py index b8b05203..0b088c03 100644 --- a/jqmc_workflow/mcmc_workflow.py +++ b/jqmc_workflow/mcmc_workflow.py @@ -230,23 +230,23 @@ def __init__( num_mcmc_bin_blocks: int = 5, num_mcmc_warmup_steps: int = 0, # -- [mcmc] section parameters -- - Dt: Optional[float] = None, - epsilon_AS: Optional[float] = None, - num_mcmc_per_measurement: Optional[int] = None, - atomic_force: Optional[bool] = None, - use_swct: Optional[bool] = None, - parameter_derivatives: Optional[bool] = None, + Dt: float | None = None, + epsilon_AS: float | None = None, + num_mcmc_per_measurement: int | None = None, + atomic_force: bool | None = None, + use_swct: bool | None = None, + parameter_derivatives: bool | None = None, # -- [control] section parameters -- - mcmc_seed: Optional[int] = None, - verbosity: Optional[str] = None, + mcmc_seed: int | None = None, + verbosity: str | None = None, # -- workflow parameters -- poll_interval: int = 60, target_error: float = 0.001, - num_mcmc_steps: Optional[int] = None, + num_mcmc_steps: int | None = None, pilot_steps: int = 100, - pilot_queue_label: Optional[str] = None, + pilot_queue_label: str | None = None, max_continuation: int = 1, - cleanup_patterns: Optional[list] = None, + cleanup_patterns: list | None = None, # -- [precision] section -- precision_mode: str = "full", ): @@ -892,7 +892,7 @@ async def _launch_auto(self, _wd): # -- Utility methods ------------------------------------------- - def _find_restart_chk(self, work_dir: str) -> Optional[str]: + def _find_restart_chk(self, work_dir: str) -> str | None: """Locate the MCMC restart checkpoint file in *work_dir*.""" for pattern in ["restart.h5", "mcmc.h5", "*.h5"]: matches = sorted(glob.glob(os.path.join(work_dir, pattern))) @@ -900,7 +900,7 @@ def _find_restart_chk(self, work_dir: str) -> Optional[str]: return os.path.basename(matches[-1]) return None - def _compute_energy(self, restart_chk: str, work_dir: str, output_file: Optional[str] = None): + def _compute_energy(self, restart_chk: str, work_dir: str, output_file: str | None = None): """Parse energy from *output_file* or run ``jqmc-tool mcmc compute-energy``. When *output_file* is given the energy is read directly from @@ -962,7 +962,7 @@ def _parse_energy_output(text: str): return float(match.group(1)), float(match.group(2)) return None, None - def _compute_force(self, restart_chk: str, work_dir: str, output_file: Optional[str] = None): + def _compute_force(self, restart_chk: str, work_dir: str, output_file: str | None = None): """Parse forces from *output_file* or run ``jqmc-tool mcmc compute-force``. When *output_file* is given, forces are read directly from diff --git a/jqmc_workflow/vmc_workflow.py b/jqmc_workflow/vmc_workflow.py index 21debb1d..489faf7a 100644 --- a/jqmc_workflow/vmc_workflow.py +++ b/jqmc_workflow/vmc_workflow.py @@ -278,39 +278,39 @@ def __init__( number_of_walkers: int = 4, max_time: int = 86400, # -- [vmc] section parameters -- - Dt: Optional[float] = None, - epsilon_AS: Optional[float] = None, - num_mcmc_per_measurement: Optional[int] = None, - num_mcmc_warmup_steps: Optional[int] = None, - num_mcmc_bin_blocks: Optional[int] = None, - wf_dump_freq: Optional[int] = None, - opt_J1_param: Optional[bool] = None, - opt_J2_param: Optional[bool] = None, - opt_J3_param: Optional[bool] = None, - opt_JNN_param: Optional[bool] = None, - opt_lambda_param: Optional[bool] = None, - opt_with_projected_MOs: Optional[bool] = None, - opt_J3_basis_exp: Optional[bool] = None, - opt_J3_basis_coeff: Optional[bool] = None, - opt_lambda_basis_exp: Optional[bool] = None, - opt_lambda_basis_coeff: Optional[bool] = None, - optimizer_kwargs: Optional[dict] = None, + Dt: float | None = None, + epsilon_AS: float | None = None, + num_mcmc_per_measurement: int | None = None, + num_mcmc_warmup_steps: int | None = None, + num_mcmc_bin_blocks: int | None = None, + wf_dump_freq: int | None = None, + opt_J1_param: bool | None = None, + opt_J2_param: bool | None = None, + opt_J3_param: bool | None = None, + opt_JNN_param: bool | None = None, + opt_lambda_param: bool | None = None, + opt_with_projected_MOs: bool | None = None, + opt_J3_basis_exp: bool | None = None, + opt_J3_basis_coeff: bool | None = None, + opt_lambda_basis_exp: bool | None = None, + opt_lambda_basis_coeff: bool | None = None, + optimizer_kwargs: dict | None = None, # -- [control] section parameters -- - mcmc_seed: Optional[int] = None, - verbosity: Optional[str] = None, + mcmc_seed: int | None = None, + verbosity: str | None = None, # -- workflow parameters -- poll_interval: int = 60, target_error: float = 0.001, - num_mcmc_steps: Optional[int] = None, + num_mcmc_steps: int | None = None, pilot_mcmc_steps: int = 50, pilot_vmc_steps: int = 5, - pilot_queue_label: Optional[str] = None, + pilot_queue_label: str | None = None, max_continuation: int = 1, - target_snr: Optional[float] = None, + target_snr: float | None = None, snr_avg_window: int = 5, - energy_slope_sigma_threshold: Optional[float] = None, + energy_slope_sigma_threshold: float | None = None, energy_slope_window_size: int = 5, - cleanup_patterns: Optional[list] = None, + cleanup_patterns: list | None = None, # -- [precision] section -- precision_mode: str = "full", ): @@ -958,7 +958,7 @@ def _check_convergence( converged = converged_snr and converged_slope return converged, converged_snr, converged_slope - def _find_restart_chk(self, work_dir: str) -> Optional[str]: + def _find_restart_chk(self, work_dir: str) -> str | None: """Locate a VMC restart checkpoint file in *work_dir*.""" for pattern in ["restart.h5", "vmc.h5", "*.h5"]: matches = sorted(glob.glob(os.path.join(work_dir, pattern))) @@ -978,7 +978,7 @@ def _parse_output(self, output_file=None): energy_pattern = re.compile(r"E\s*=\s*([+-]?\d+\.\d+(?:[eE][+-]?\d+)?)\s*\+\-\s*(\d+\.\d+(?:[eE][+-]?\d+)?)") last_match = None try: - with open(output_file, "r") as f: + with open(output_file) as f: for line in f: m = energy_pattern.search(line) if m: @@ -1008,7 +1008,7 @@ def _parse_all_snr(output_file): snr_pattern = re.compile(r"Max of signal-to-noise of f = max\(\|f\|/\|std f\|\) = ([-+]?\d+(?:\.\d+)?)") values = [] try: - with open(output_file, "r") as f: + with open(output_file) as f: for line in f: m = snr_pattern.search(line) if m: @@ -1033,7 +1033,7 @@ def _parse_all_energies(output_file: str) -> list[tuple[float, float]]: if not os.path.isfile(output_file): return [] try: - with open(output_file, "r") as f: + with open(output_file) as f: text = f.read() from ._output_parser import _parse_vmc_log_text @@ -1101,7 +1101,7 @@ def _parse_last_opt_energy(output_file): energy_pattern = re.compile(r"E\s*=\s*([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?)\s*\+\-\s*(\d+\.?\d*(?:[eE][+-]?\d+)?)") last_match = None try: - with open(output_file, "r") as f: + with open(output_file) as f: for line in f: m = energy_pattern.search(line) if m: diff --git a/jqmc_workflow/wf_workflow.py b/jqmc_workflow/wf_workflow.py index 6a5cd25b..fa29f56a 100644 --- a/jqmc_workflow/wf_workflow.py +++ b/jqmc_workflow/wf_workflow.py @@ -113,14 +113,14 @@ def __init__( self, trexio_file: str = "trexio.h5", hamiltonian_file: str = "hamiltonian_data.h5", - j1_parameter: Optional[float] = None, - j1_type: Optional[str] = None, - j2_parameter: Optional[float] = None, - j2_type: Optional[str] = None, - j3_basis_type: Optional[str] = None, - j_nn_type: Optional[str] = None, - j_nn_params: Optional[List[str]] = None, - ao_conv_to: Optional[str] = None, + j1_parameter: float | None = None, + j1_type: str | None = None, + j2_parameter: float | None = None, + j2_type: str | None = None, + j3_basis_type: str | None = None, + j_nn_type: str | None = None, + j_nn_params: list[str] | None = None, + ao_conv_to: str | None = None, ): super().__init__() self.trexio_file = trexio_file diff --git a/jqmc_workflow/workflow.py b/jqmc_workflow/workflow.py index 70099e19..d2e8cf2d 100644 --- a/jqmc_workflow/workflow.py +++ b/jqmc_workflow/workflow.py @@ -250,14 +250,14 @@ async def run(self): return self.status, ["result.h5"], {"energy": -1.23} """ - def __init__(self, project_dir: Optional[str] = None, cleanup_patterns: Optional[List[str]] = None): + def __init__(self, project_dir: str | None = None, cleanup_patterns: list[str] | None = None): self.status: WorkflowStatus = WorkflowStatus.PENDING self.phase: ScientificPhase = ScientificPhase.INIT - self.output_files: List[str] = [] + self.output_files: list[str] = [] self.output_values: dict = {} - self.project_dir: Optional[str] = os.path.abspath(project_dir) if project_dir else None - self._bg_task: Optional[asyncio.Task] = None - self.cleanup_patterns: List[str] = cleanup_patterns or [] + self.project_dir: str | None = os.path.abspath(project_dir) if project_dir else None + self._bg_task: asyncio.Task | None = None + self.cleanup_patterns: list[str] = cleanup_patterns or [] # -- Filename generation (per-job run_id) ---------------------- @@ -696,11 +696,11 @@ class Container: def __init__( self, - label: Optional[str] = "workflow", - dirname: Optional[str] = "workflow", - input_files: Optional[list] = None, - rename_input_files: Optional[list] = None, - workflow: Optional[Workflow] = None, + label: str | None = "workflow", + dirname: str | None = "workflow", + input_files: list | None = None, + rename_input_files: list | None = None, + workflow: Workflow | None = None, ): self.label = label self.dirname = dirname @@ -709,10 +709,10 @@ def __init__( self.workflow = workflow or Workflow() # Output placeholders (populated after launch) - self.output_files: List[str] = [] + self.output_files: list[str] = [] self.output_values: dict = {} self.status = "init" - self._bg_task: Optional[asyncio.Task] = None + self._bg_task: asyncio.Task | None = None # Directories self.root_dir = os.getcwd() diff --git a/pyproject.toml b/pyproject.toml index 00c145e0..07a2156a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,8 +68,7 @@ lint.extend-ignore = [ # flake8-print "T201", # pyupgrade - "UP006", "UP007", "UP009", "UP015", "UP022", "UP024", "UP031", - "UP032", "UP033", "UP034", "UP035", "UP037", "UP045", + "UP022", "UP031", "UP035", # pycodestyle warnings "W291", "W293", "W605", ] diff --git a/tests/test_jqmc_command_lines.py b/tests/test_jqmc_command_lines.py index dd48f415..d7ad04db 100644 --- a/tests/test_jqmc_command_lines.py +++ b/tests/test_jqmc_command_lines.py @@ -87,7 +87,7 @@ def test_jqmc_cli_run_mcmc(tmp_path, monkeypatch, trexio_file: str): # generate input os.chdir(root_dir) mcmc_generate_input(flag=True, filename=os.path.join(tmp_path, "mcmc_input.toml"), exclude_comment=True) - with open(os.path.join(tmp_path, "mcmc_input.toml"), "r") as f: + with open(os.path.join(tmp_path, "mcmc_input.toml")) as f: dict_toml = toml.load(f) dict_toml["control"]["restart"] = False dict_toml["control"]["hamiltonian_h5"] = "hamiltonian_data.h5" @@ -116,7 +116,7 @@ def test_jqmc_cli_run_mcmc(tmp_path, monkeypatch, trexio_file: str): # run MCMC(restart) os.chdir(root_dir) - with open(os.path.join(tmp_path, "mcmc_input.toml"), "r") as f: + with open(os.path.join(tmp_path, "mcmc_input.toml")) as f: dict_toml = toml.load(f) dict_toml["control"]["restart"] = True dict_toml["control"]["hamiltonian_h5"] = None @@ -148,7 +148,7 @@ def test_jqmc_cli_run_vmc(tmp_path, monkeypatch, trexio_file: str): # generate input os.chdir(root_dir) vmc_generate_input(flag=True, filename=os.path.join(tmp_path, "vmc_input.toml"), exclude_comment=True) - with open(os.path.join(tmp_path, "vmc_input.toml"), "r") as f: + with open(os.path.join(tmp_path, "vmc_input.toml")) as f: dict_toml = toml.load(f) dict_toml["control"]["restart"] = False dict_toml["control"]["hamiltonian_h5"] = "hamiltonian_data.h5" @@ -170,7 +170,7 @@ def test_jqmc_cli_run_vmc(tmp_path, monkeypatch, trexio_file: str): # run VMCopt(restart) os.chdir(root_dir) - with open(os.path.join(tmp_path, "vmc_input.toml"), "r") as f: + with open(os.path.join(tmp_path, "vmc_input.toml")) as f: dict_toml = toml.load(f) dict_toml["control"]["restart"] = True dict_toml["control"]["hamiltonian_chk"] = None @@ -208,7 +208,7 @@ def test_jqmc_cli_run_lrdmc(tmp_path, monkeypatch, trexio_file: str): ) os.chdir(root_dir) lrdmc_generate_input(flag=True, filename=os.path.join(tmp_alat_path, "lrdmc_input.toml"), exclude_comment=True) - with open(os.path.join(tmp_alat_path, "lrdmc_input.toml"), "r") as f: + with open(os.path.join(tmp_alat_path, "lrdmc_input.toml")) as f: dict_toml = toml.load(f) dict_toml["control"]["restart"] = False dict_toml["control"]["hamiltonian_h5"] = "hamiltonian_data.h5" @@ -262,7 +262,7 @@ def test_jqmc_cli_run_lrdmc(tmp_path, monkeypatch, trexio_file: str): os.chdir(root_dir) for alat in alat_list: tmp_alat_path = os.path.join(tmp_path, str(alat)) - with open(os.path.join(tmp_alat_path, "lrdmc_input.toml"), "r") as f: + with open(os.path.join(tmp_alat_path, "lrdmc_input.toml")) as f: dict_toml = toml.load(f) dict_toml["control"]["restart"] = True dict_toml["control"]["hamiltonian_h5"] = None diff --git a/tests/trexio_example_files/read_jastrow_factor_from_turbo_wf.py b/tests/trexio_example_files/read_jastrow_factor_from_turbo_wf.py index 6c59e529..8eb1a56e 100644 --- a/tests/trexio_example_files/read_jastrow_factor_from_turbo_wf.py +++ b/tests/trexio_example_files/read_jastrow_factor_from_turbo_wf.py @@ -158,7 +158,7 @@ assert max_row == max_col const_jas_orb_index = max_row - j1_matrix = np.zeros((jas_aos_data.num_ao)) + j1_matrix = np.zeros(jas_aos_data.num_ao) for i, (row, col) in enumerate(zip(f10jasmatrix.row, f10jasmatrix.col)): if col == const_jas_orb_index and row != const_jas_orb_index: diff --git a/tools/read_Jastrow_factor_from_turbo_wf.py b/tools/read_Jastrow_factor_from_turbo_wf.py index ebddba87..320e64db 100644 --- a/tools/read_Jastrow_factor_from_turbo_wf.py +++ b/tools/read_Jastrow_factor_from_turbo_wf.py @@ -119,7 +119,7 @@ def to_jsonable(x): assert max_row == max_col const_jas_orb_index = max_row -j1_matrix = np.zeros((ao_num_count)) +j1_matrix = np.zeros(ao_num_count) for i, (row, col) in enumerate(zip(f10jasmatrix.row, f10jasmatrix.col)): if col == const_jas_orb_index and row != const_jas_orb_index: From 01ebc1b2240a2db0e857f9dcef0d365adf6238fa Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 01:37:20 +0900 Subject: [PATCH 48/97] ruff: apply the third manual fix --- doc/conf.py | 2 +- jqmc/hamiltonians.py | 2 +- jqmc/jqmc_tool.py | 1 - jqmc_workflow/_lrdmc_calibration.py | 1 - jqmc_workflow/_machine.py | 4 +--- jqmc_workflow/launcher.py | 1 - jqmc_workflow/lrdmc_ext_workflow.py | 1 - jqmc_workflow/wf_workflow.py | 1 - jqmc_workflow/workflow.py | 1 - pyproject.toml | 2 -- 10 files changed, 3 insertions(+), 13 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 2fa9827d..521566a5 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -276,7 +276,7 @@ def _dedup_footnote(m): # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -html_title = "jQMC v%s" % release +html_title = f"jQMC v{release}" # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None diff --git a/jqmc/hamiltonians.py b/jqmc/hamiltonians.py index 5f629cd4..1d88a144 100644 --- a/jqmc/hamiltonians.py +++ b/jqmc/hamiltonians.py @@ -44,7 +44,7 @@ import dataclasses import importlib from logging import getLogger -from typing import Any, Type, TypeVar, Union +from typing import Any, TypeVar import h5py diff --git a/jqmc/jqmc_tool.py b/jqmc/jqmc_tool.py index 24623e59..23bd3fbe 100644 --- a/jqmc/jqmc_tool.py +++ b/jqmc/jqmc_tool.py @@ -44,7 +44,6 @@ import sys from enum import Enum from logging import Formatter, StreamHandler, getLogger -from typing import List import click import jax.numpy as jnp diff --git a/jqmc_workflow/_lrdmc_calibration.py b/jqmc_workflow/_lrdmc_calibration.py index 70fdaf03..f3c7a1dd 100644 --- a/jqmc_workflow/_lrdmc_calibration.py +++ b/jqmc_workflow/_lrdmc_calibration.py @@ -49,7 +49,6 @@ import math import re from logging import getLogger -from typing import List, Optional import h5py diff --git a/jqmc_workflow/_machine.py b/jqmc_workflow/_machine.py index 96245be4..92448380 100644 --- a/jqmc_workflow/_machine.py +++ b/jqmc_workflow/_machine.py @@ -46,7 +46,6 @@ import time from concurrent.futures import ThreadPoolExecutor from logging import getLogger -from subprocess import PIPE import paramiko import yaml @@ -353,8 +352,7 @@ def _run_local(self, command_r: str, max_retries: int = 10): proc = subprocess.run( command_r, shell=True, - stdout=PIPE, - stderr=PIPE, + capture_output=True, text=True, timeout=1200, ) diff --git a/jqmc_workflow/launcher.py b/jqmc_workflow/launcher.py index 20e88411..818b76ed 100644 --- a/jqmc_workflow/launcher.py +++ b/jqmc_workflow/launcher.py @@ -47,7 +47,6 @@ StreamHandler, getLogger, ) -from typing import List, Optional from .workflow import ( Container, diff --git a/jqmc_workflow/lrdmc_ext_workflow.py b/jqmc_workflow/lrdmc_ext_workflow.py index 2c323e1b..d4e89f1b 100644 --- a/jqmc_workflow/lrdmc_ext_workflow.py +++ b/jqmc_workflow/lrdmc_ext_workflow.py @@ -46,7 +46,6 @@ import re import subprocess from logging import getLogger -from typing import Dict, List, Optional, Union from ._setting import ( GFMC_MIN_BIN_BLOCKS, diff --git a/jqmc_workflow/wf_workflow.py b/jqmc_workflow/wf_workflow.py index fa29f56a..d400cf99 100644 --- a/jqmc_workflow/wf_workflow.py +++ b/jqmc_workflow/wf_workflow.py @@ -43,7 +43,6 @@ import shlex import subprocess from logging import getLogger -from typing import List, Optional from ._state import WorkflowStatus from .workflow import Workflow diff --git a/jqmc_workflow/workflow.py b/jqmc_workflow/workflow.py index d2e8cf2d..6a4714b9 100644 --- a/jqmc_workflow/workflow.py +++ b/jqmc_workflow/workflow.py @@ -42,7 +42,6 @@ import shutil import uuid from logging import getLogger -from typing import List, Optional from ._job import JobSubmission from ._phase import ScientificPhase, require_action diff --git a/pyproject.toml b/pyproject.toml index 07a2156a..1961674b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,8 +67,6 @@ lint.extend-ignore = [ "SIM101", "SIM102", "SIM103", "SIM105", "SIM114", "SIM115", "SIM118", "SIM300", # flake8-print "T201", - # pyupgrade - "UP022", "UP031", "UP035", # pycodestyle warnings "W291", "W293", "W605", ] From 2e4cfdf690f4964a1a5fc0467f7a3f8806b1f6c2 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 08:49:34 +0900 Subject: [PATCH 49/97] ruff: apply the auto fix again. --- benchmarks/benchmark_gfmc_kernels.py | 2 +- benchmarks/profile_bottleneck.py | 2 +- .../01_S22_water_monomer_1/01DFT/run_pyscf.py | 6 +- .../02_S22_water_monomer_2/01DFT/run_pyscf.py | 6 +- .../03_S22_water_dimer/01DFT/run_pyscf.py | 6 +- .../jqmc-workflow-example02/run_pipelines.py | 4 +- .../jqmc-workflow-example03/run_pipelines.py | 4 +- .../run_test_on_local.py | 2 +- examples/sync_examples.py | 3 +- jqmc/_jqmc_utility.py | 2 +- jqmc/atomic_orbital.py | 8 +- jqmc/coulomb_potential.py | 3 +- jqmc/determinant.py | 105 ++++++++---------- jqmc/hamiltonians.py | 30 +++-- jqmc/jastrow_factor.py | 31 ++---- jqmc/jqmc_cli.py | 14 +-- jqmc/jqmc_gfmc.py | 21 ++-- jqmc/jqmc_mcmc.py | 7 +- jqmc/jqmc_tool.py | 3 +- jqmc/molecular_orbital.py | 1 - jqmc/trexio_wrapper.py | 7 +- jqmc/wavefunction.py | 2 - jqmc_workflow/_cli.py | 3 +- jqmc_workflow/_error_estimator.py | 10 +- jqmc_workflow/_input_generator.py | 19 ++-- jqmc_workflow/_job.py | 4 +- jqmc_workflow/_lrdmc_calibration.py | 12 +- jqmc_workflow/_machine.py | 3 +- jqmc_workflow/_output_parser.py | 17 ++- jqmc_workflow/_results.py | 13 +-- jqmc_workflow/_state.py | 2 +- jqmc_workflow/launcher.py | 17 ++- jqmc_workflow/lrdmc_ext_workflow.py | 12 +- jqmc_workflow/lrdmc_workflow.py | 15 ++- jqmc_workflow/mcmc_workflow.py | 13 +-- jqmc_workflow/vmc_workflow.py | 21 ++-- jqmc_workflow/wf_workflow.py | 8 +- jqmc_workflow/workflow.py | 43 ++++--- pyproject.toml | 23 ++-- tests/test_AOs.py | 99 ++++++++--------- tests/test_MOs.py | 16 ++- tests/test_ao_basis_optimization.py | 4 +- tests/test_checkpoint_components.py | 2 - tests/test_determinant.py | 3 +- tests/test_init_electron_configurations.py | 3 +- tests/test_jastrow.py | 3 +- tests/test_jqmc_gfmc_bra.py | 2 +- tests/test_jqmc_gfmc_tau.py | 2 +- tests/test_jqmc_mcmc.py | 4 +- tests/test_wave_function.py | 18 ++- tools/_read_from_turbo_wf_and_ecp.py | 2 - tools/read_Jastrow_factor_from_turbo_wf.py | 1 - 52 files changed, 306 insertions(+), 357 deletions(-) diff --git a/benchmarks/benchmark_gfmc_kernels.py b/benchmarks/benchmark_gfmc_kernels.py index e6b0a6a7..c6d2aa72 100644 --- a/benchmarks/benchmark_gfmc_kernels.py +++ b/benchmarks/benchmark_gfmc_kernels.py @@ -48,7 +48,7 @@ import jax.scipy.linalg as jsp_linalg import numpy as np from functools import partial -from jax import jit, lax, vmap +from jax import jit, vmap from jqmc.atomic_orbital import AOs_cart_data from jqmc.coulomb_potential import ( diff --git a/benchmarks/profile_bottleneck.py b/benchmarks/profile_bottleneck.py index 7f82f8b9..6af25d65 100644 --- a/benchmarks/profile_bottleneck.py +++ b/benchmarks/profile_bottleneck.py @@ -65,7 +65,7 @@ def timefull(fn, *a): print(f"n_up={n_up}, n_dn={n_dn}") print(f"n_AO={gd.orb_data_up_spin.aos_data.num_ao}, n_prim={gd.orb_data_up_spin.aos_data.num_ao_prim}") -print(f"\nFull functions:") +print("\nFull functions:") print(f" kinetic_discretized: {t_kin:.3f} ms") print(f" ecp_non_local_tmove: {t_ecp:.3f} ms") diff --git a/examples/jqmc-example04/01_S22_water_monomer_1/01DFT/run_pyscf.py b/examples/jqmc-example04/01_S22_water_monomer_1/01DFT/run_pyscf.py index d49255c8..f482c386 100644 --- a/examples/jqmc-example04/01_S22_water_monomer_1/01DFT/run_pyscf.py +++ b/examples/jqmc-example04/01_S22_water_monomer_1/01DFT/run_pyscf.py @@ -1,11 +1,11 @@ from pyscf import gto, scf from pyscf.tools import trexio -filename = f"water_monomer_1.h5" +filename = "water_monomer_1.h5" mol = gto.Mole() mol.verbose = 5 -mol.atom = f""" +mol.atom = """ O -1.551007 -0.114520 0.000000 H -1.934259 0.762503 0.000000 H -0.599677 0.040712 0.000000 @@ -17,7 +17,7 @@ mol.spin = 0 mol.symmetry = False mol.cart = True -mol.output = f"water_monomer_1.out" +mol.output = "water_monomer_1.out" mol.build() mf = scf.KS(mol).density_fit() diff --git a/examples/jqmc-example04/02_S22_water_monomer_2/01DFT/run_pyscf.py b/examples/jqmc-example04/02_S22_water_monomer_2/01DFT/run_pyscf.py index aa4ddfa6..6e5b39c4 100644 --- a/examples/jqmc-example04/02_S22_water_monomer_2/01DFT/run_pyscf.py +++ b/examples/jqmc-example04/02_S22_water_monomer_2/01DFT/run_pyscf.py @@ -1,11 +1,11 @@ from pyscf import gto, scf from pyscf.tools import trexio -filename = f"water_monomer_2.h5" +filename = "water_monomer_2.h5" mol = gto.Mole() mol.verbose = 5 -mol.atom = f""" +mol.atom = """ O 1.350625 0.111469 0.000000 H 1.680398 -0.373741 -0.758561 H 1.680398 -0.373741 0.758561 @@ -17,7 +17,7 @@ mol.spin = 0 mol.symmetry = False mol.cart = True -mol.output = f"water_monomer_2.out" +mol.output = "water_monomer_2.out" mol.build() mf = scf.KS(mol).density_fit() diff --git a/examples/jqmc-example04/03_S22_water_dimer/01DFT/run_pyscf.py b/examples/jqmc-example04/03_S22_water_dimer/01DFT/run_pyscf.py index 0af64319..88102fe5 100644 --- a/examples/jqmc-example04/03_S22_water_dimer/01DFT/run_pyscf.py +++ b/examples/jqmc-example04/03_S22_water_dimer/01DFT/run_pyscf.py @@ -1,11 +1,11 @@ from pyscf import gto, scf from pyscf.tools import trexio -filename = f"water_dimer.h5" +filename = "water_dimer.h5" mol = gto.Mole() mol.verbose = 5 -mol.atom = f""" +mol.atom = """ O -1.551007 -0.114520 0.000000 H -1.934259 0.762503 0.000000 H -0.599677 0.040712 0.000000 @@ -20,7 +20,7 @@ mol.spin = 0 mol.symmetry = False mol.cart = True -mol.output = f"water_dimer.out" +mol.output = "water_dimer.out" mol.build() mf = scf.KS(mol).density_fit() diff --git a/examples/jqmc-workflow-example02/run_pipelines.py b/examples/jqmc-workflow-example02/run_pipelines.py index 53d2d98e..562e40b4 100644 --- a/examples/jqmc-workflow-example02/run_pipelines.py +++ b/examples/jqmc-workflow-example02/run_pipelines.py @@ -144,7 +144,7 @@ def run_pyscf(base_dir: str) -> float | None: if os.path.isfile(trexio_path): print(f" [skip] {TREXIO_FILE} already exists.") else: - print(f" [run] pySCF for water ...") + print(" [run] pySCF for water ...") script_path = os.path.join(base_dir, "_local_pyscf.py") with open(script_path, "w") as f: f.write( @@ -313,7 +313,7 @@ def print_summary_table( """Print a summary table of energies and wall times.""" print() print("=" * 120) - print(f" Walker-scaling Benchmark Summary (water, ccECP/cc-pVTZ, JSD)") + print(" Walker-scaling Benchmark Summary (water, ccECP/cc-pVTZ, JSD)") print( f" MCMC: {NUM_MCMC_STEPS_MCMC} steps, LRDMC: {NUM_MCMC_STEPS_LRDMC} steps (nmpm={NUM_MCMC_PER_MEASUREMENT}, a={ALAT})" ) diff --git a/examples/jqmc-workflow-example03/run_pipelines.py b/examples/jqmc-workflow-example03/run_pipelines.py index 85deec14..cb8163ac 100644 --- a/examples/jqmc-workflow-example03/run_pipelines.py +++ b/examples/jqmc-workflow-example03/run_pipelines.py @@ -125,7 +125,7 @@ def run_pyscf(base_dir: str) -> float | None: if os.path.isfile(trexio_path): print(f" [skip] {TREXIO_FILE} already exists.") else: - print(f" [run] pySCF for water ...") + print(" [run] pySCF for water ...") script_path = os.path.join(base_dir, "_local_pyscf.py") with open(script_path, "w") as f: f.write( @@ -160,7 +160,7 @@ def build_pipeline() -> tuple[ ]: """Build Container list for J3 + MCMC / LRDMC. - Returns + Returns: ------- all_workflows : list[Container] Flat list of all containers (for Launcher). diff --git a/examples/jqmc-workflow-example99/run_test_on_local.py b/examples/jqmc-workflow-example99/run_test_on_local.py index 00894eac..e5055a1d 100644 --- a/examples/jqmc-workflow-example99/run_test_on_local.py +++ b/examples/jqmc-workflow-example99/run_test_on_local.py @@ -317,7 +317,7 @@ def _dump_dataclass(label, obj): """Print all fields of a dataclass, truncating stderr_tail.""" d = dataclasses.asdict(obj) # Truncate stderr_tail for readability - if "stderr_tail" in d and d["stderr_tail"]: + if d.get("stderr_tail"): d["stderr_tail"] = f"<{len(d['stderr_tail'])} chars>" # For VMC steps, show compact per-step summaries if "steps" in d and isinstance(d["steps"], list): diff --git a/examples/sync_examples.py b/examples/sync_examples.py index 90240dab..d9733a30 100644 --- a/examples/sync_examples.py +++ b/examples/sync_examples.py @@ -4,8 +4,7 @@ def sync_readme(readme_path): - """ - Reads a README file, looks for markers, + """Reads a README file, looks for markers, and updates the following code block with the content of the file. """ try: diff --git a/jqmc/_jqmc_utility.py b/jqmc/_jqmc_utility.py index 3cccd021..da207123 100644 --- a/jqmc/_jqmc_utility.py +++ b/jqmc/_jqmc_utility.py @@ -38,7 +38,7 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from functools import lru_cache, cache +from functools import cache from logging import getLogger import numpy as np diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index 612ebc54..d5ffba95 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -1363,7 +1363,7 @@ class ShellPrimMap: num_full: Total number of primitives in the full array. """ - __slots__ = ("unique_indices", "prim_to_unique", "num_unique", "num_full") + __slots__ = ("num_full", "num_unique", "prim_to_unique", "unique_indices") def __init__(self, unique_indices: np.ndarray, prim_to_unique: np.ndarray): """Build the prim<->unique-AO index mapping from precomputed arrays.""" @@ -3055,7 +3055,7 @@ def A_m(x: float, y: float) -> float: return np.sum( [ scipy.special.binom(m_abs, p) * x ** (p) * y ** (m_abs - p) * np.cos((m_abs - p) * (np.pi / 2.0)) - for p in range(0, m_abs + 1) + for p in range(m_abs + 1) ] ) @@ -3063,7 +3063,7 @@ def B_m(x: float, y: float) -> float: return np.sum( [ scipy.special.binom(m_abs, p) * x ** (p) * y ** (m_abs - p) * np.sin((m_abs - p) * (np.pi / 2.0)) - for p in range(0, m_abs + 1) + for p in range(m_abs + 1) ] ) @@ -3083,7 +3083,7 @@ def lambda_lm(k: int) -> float: def Lambda_lm(r_norm: float, z: float) -> float: return np.sqrt( (2 - int(m_abs == 0)) * scipy.special.factorial(l - m_abs) / scipy.special.factorial(l + m_abs) - ) * np.sum([lambda_lm(k) * r_norm ** (2 * k) * z ** (l - 2 * k - m_abs) for k in range(0, int((l - m_abs) / 2) + 1)]) + ) * np.sum([lambda_lm(k) * r_norm ** (2 * k) * z ** (l - 2 * k - m_abs) for k in range(int((l - m_abs) / 2) + 1)]) # solid harmonics eveluated in Cartesian coord. (x,y,z): if m >= 0: diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 6d9d1b3c..36007951 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -320,8 +320,7 @@ def _effective_charges(self) -> npt.NDArray: """ if self.ecp_flag: return np.array(self.structure_data.atomic_numbers) - np.array(self.z_cores) - else: - return np.array(self.structure_data.atomic_numbers) + return np.array(self.structure_data.atomic_numbers) @property def _global_max_ang_mom_plus_1(self) -> int: diff --git a/jqmc/determinant.py b/jqmc/determinant.py index f7431ccd..572d214c 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -184,60 +184,54 @@ def ao_exponents_up(self) -> jax.Array: """AO Gaussian exponents for spin-up orbitals (jnp view of underlying numpy storage).""" if isinstance(self.orb_data_up_spin, (AOs_sphe_data, AOs_cart_data)): return self.orb_data_up_spin._exponents_jnp - elif isinstance(self.orb_data_up_spin, MOs_data): + if isinstance(self.orb_data_up_spin, MOs_data): return self.orb_data_up_spin.aos_data._exponents_jnp - else: - raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_up_spin)}") + raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_up_spin)}") @property def ao_exponents_dn(self) -> jax.Array: """AO Gaussian exponents for spin-down orbitals (jnp view of underlying numpy storage).""" if isinstance(self.orb_data_dn_spin, (AOs_sphe_data, AOs_cart_data)): return self.orb_data_dn_spin._exponents_jnp - elif isinstance(self.orb_data_dn_spin, MOs_data): + if isinstance(self.orb_data_dn_spin, MOs_data): return self.orb_data_dn_spin.aos_data._exponents_jnp - else: - raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_dn_spin)}") + raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_dn_spin)}") @property def ao_coefficients_up(self) -> jax.Array: """AO contraction coefficients for spin-up orbitals (jnp view of underlying numpy storage).""" if isinstance(self.orb_data_up_spin, (AOs_sphe_data, AOs_cart_data)): return self.orb_data_up_spin._coefficients_jnp - elif isinstance(self.orb_data_up_spin, MOs_data): + if isinstance(self.orb_data_up_spin, MOs_data): return self.orb_data_up_spin.aos_data._coefficients_jnp - else: - raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_up_spin)}") + raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_up_spin)}") @property def ao_coefficients_dn(self) -> jax.Array: """AO contraction coefficients for spin-down orbitals (jnp view of underlying numpy storage).""" if isinstance(self.orb_data_dn_spin, (AOs_sphe_data, AOs_cart_data)): return self.orb_data_dn_spin._coefficients_jnp - elif isinstance(self.orb_data_dn_spin, MOs_data): + if isinstance(self.orb_data_dn_spin, MOs_data): return self.orb_data_dn_spin.aos_data._coefficients_jnp - else: - raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_dn_spin)}") + raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data_dn_spin)}") def _replace_orb_exponents(self, orb_data, new_exp): """Helper to replace exponents in an orb_data (AO or MO).""" if isinstance(orb_data, (AOs_sphe_data, AOs_cart_data)): return orb_data.replace(exponents=new_exp) - elif isinstance(orb_data, MOs_data): + if isinstance(orb_data, MOs_data): new_aos = orb_data.aos_data.replace(exponents=new_exp) return orb_data.replace(aos_data=new_aos) - else: - raise NotImplementedError(f"Unsupported orb_data type: {type(orb_data)}") + raise NotImplementedError(f"Unsupported orb_data type: {type(orb_data)}") def _replace_orb_coefficients(self, orb_data, new_coeff): """Helper to replace coefficients in an orb_data (AO or MO).""" if isinstance(orb_data, (AOs_sphe_data, AOs_cart_data)): return orb_data.replace(coefficients=new_coeff) - elif isinstance(orb_data, MOs_data): + if isinstance(orb_data, MOs_data): new_aos = orb_data.aos_data.replace(coefficients=new_coeff) return orb_data.replace(aos_data=new_aos) - else: - raise NotImplementedError(f"Unsupported orb_data type: {type(orb_data)}") + raise NotImplementedError(f"Unsupported orb_data type: {type(orb_data)}") def with_updated_ao_exponents( self, new_exp_up: npt.NDArray[np.float64], new_exp_dn: npt.NDArray[np.float64] @@ -300,13 +294,13 @@ def apply_block_update(self, block: "VariationalParameterBlock") -> "Geminal_dat orb_data_dn_spin=self.orb_data_dn_spin, lambda_matrix=lambda_new, ) - elif block.name == "lambda_basis_exp": + if block.name == "lambda_basis_exp": vals = np.asarray(block.values, dtype=np.float64) vals = self._symmetrize_ao_basis(vals) n_up = len(self.ao_exponents_up) new_exp_up, new_exp_dn = vals[:n_up], vals[n_up:] return self.with_updated_ao_exponents(new_exp_up, new_exp_dn) - elif block.name == "lambda_basis_coeff": + if block.name == "lambda_basis_coeff": vals = np.asarray(block.values, dtype=np.float64) vals = self._symmetrize_ao_basis(vals) n_up = len(self.ao_coefficients_up) @@ -415,10 +409,9 @@ def orb_num_up(self) -> int: """ if isinstance(self.orb_data_up_spin, AOs_sphe_data) or isinstance(self.orb_data_up_spin, AOs_cart_data): return self.orb_data_up_spin.num_ao - elif isinstance(self.orb_data_up_spin, MOs_data): + if isinstance(self.orb_data_up_spin, MOs_data): return self.orb_data_up_spin.num_mo - else: - raise NotImplementedError + raise NotImplementedError @property def orb_num_dn(self) -> int: @@ -436,10 +429,9 @@ def orb_num_dn(self) -> int: """ if isinstance(self.orb_data_dn_spin, AOs_sphe_data) or isinstance(self.orb_data_dn_spin, AOs_cart_data): return self.orb_data_dn_spin.num_ao - elif isinstance(self.orb_data_dn_spin, MOs_data): + if isinstance(self.orb_data_dn_spin, MOs_data): return self.orb_data_dn_spin.num_mo - else: - raise NotImplementedError + raise NotImplementedError @property def is_mo_representation(self) -> bool: @@ -468,14 +460,13 @@ def compute_orb_api(self) -> Callable[..., npt.NDArray[np.float64]]: If the instances of orb_data_up_spin/orb_data_dn_spin are neither AOs_data/AOs_data nor MOs_data/MOs_data. """ - if isinstance(self.orb_data_up_spin, AOs_sphe_data) and isinstance(self.orb_data_dn_spin, AOs_sphe_data): - return compute_AOs - elif isinstance(self.orb_data_up_spin, AOs_cart_data) and isinstance(self.orb_data_dn_spin, AOs_cart_data): + if (isinstance(self.orb_data_up_spin, AOs_sphe_data) and isinstance(self.orb_data_dn_spin, AOs_sphe_data)) or ( + isinstance(self.orb_data_up_spin, AOs_cart_data) and isinstance(self.orb_data_dn_spin, AOs_cart_data) + ): return compute_AOs - elif isinstance(self.orb_data_up_spin, MOs_data) and isinstance(self.orb_data_dn_spin, MOs_data): + if isinstance(self.orb_data_up_spin, MOs_data) and isinstance(self.orb_data_dn_spin, MOs_data): return compute_MOs - else: - raise NotImplementedError + raise NotImplementedError @property def compute_orb_grad_api(self) -> Callable[..., npt.NDArray[np.float64]]: @@ -492,14 +483,13 @@ def compute_orb_grad_api(self) -> Callable[..., npt.NDArray[np.float64]]: If the instances of orb_data_up_spin/orb_data_dn_spin are neither AOs_data/AOs_data nor MOs_data/MOs_data. """ - if isinstance(self.orb_data_up_spin, AOs_sphe_data) and isinstance(self.orb_data_dn_spin, AOs_sphe_data): - return compute_AOs_grad - elif isinstance(self.orb_data_up_spin, AOs_cart_data) and isinstance(self.orb_data_dn_spin, AOs_cart_data): + if (isinstance(self.orb_data_up_spin, AOs_sphe_data) and isinstance(self.orb_data_dn_spin, AOs_sphe_data)) or ( + isinstance(self.orb_data_up_spin, AOs_cart_data) and isinstance(self.orb_data_dn_spin, AOs_cart_data) + ): return compute_AOs_grad - elif isinstance(self.orb_data_up_spin, MOs_data) and isinstance(self.orb_data_dn_spin, MOs_data): + if isinstance(self.orb_data_up_spin, MOs_data) and isinstance(self.orb_data_dn_spin, MOs_data): return compute_MOs_grad - else: - raise NotImplementedError + raise NotImplementedError @property def compute_orb_laplacian_api(self) -> Callable[..., npt.NDArray[np.float64]]: @@ -516,14 +506,13 @@ def compute_orb_laplacian_api(self) -> Callable[..., npt.NDArray[np.float64]]: If the instances of orb_data_up_spin/orb_data_dn_spin are neither AOs_data/AOs_data nor MOs_data/MOs_data. """ - if isinstance(self.orb_data_up_spin, AOs_sphe_data) and isinstance(self.orb_data_dn_spin, AOs_sphe_data): - return compute_AOs_laplacian - elif isinstance(self.orb_data_up_spin, AOs_cart_data) and isinstance(self.orb_data_dn_spin, AOs_cart_data): + if (isinstance(self.orb_data_up_spin, AOs_sphe_data) and isinstance(self.orb_data_dn_spin, AOs_sphe_data)) or ( + isinstance(self.orb_data_up_spin, AOs_cart_data) and isinstance(self.orb_data_dn_spin, AOs_cart_data) + ): return compute_AOs_laplacian - elif isinstance(self.orb_data_up_spin, MOs_data) and isinstance(self.orb_data_dn_spin, MOs_data): + if isinstance(self.orb_data_up_spin, MOs_data) and isinstance(self.orb_data_dn_spin, MOs_data): return compute_MOs_laplacian - else: - raise NotImplementedError + raise NotImplementedError @property def compute_orb_value_grad_lap_api(self) -> Callable[..., tuple]: @@ -534,14 +523,13 @@ def compute_orb_value_grad_lap_api(self) -> Callable[..., tuple]: (``exp``, polynomial chain, ``S_l_m``) is shared across val/grad/lap instead of being recomputed three times. """ - if isinstance(self.orb_data_up_spin, AOs_sphe_data) and isinstance(self.orb_data_dn_spin, AOs_sphe_data): - return compute_AOs_value_grad_lap - elif isinstance(self.orb_data_up_spin, AOs_cart_data) and isinstance(self.orb_data_dn_spin, AOs_cart_data): + if (isinstance(self.orb_data_up_spin, AOs_sphe_data) and isinstance(self.orb_data_dn_spin, AOs_sphe_data)) or ( + isinstance(self.orb_data_up_spin, AOs_cart_data) and isinstance(self.orb_data_dn_spin, AOs_cart_data) + ): return compute_AOs_value_grad_lap - elif isinstance(self.orb_data_up_spin, MOs_data) and isinstance(self.orb_data_dn_spin, MOs_data): + if isinstance(self.orb_data_up_spin, MOs_data) and isinstance(self.orb_data_dn_spin, MOs_data): return compute_MOs_value_grad_lap - else: - raise NotImplementedError + raise NotImplementedError def to_cartesian(self) -> "Geminal_data": """Convert spherical orbitals to Cartesian and transform the lambda matrix. @@ -686,15 +674,15 @@ def projection_operators(self) -> tuple: @classmethod def convert_from_MOs_to_AOs(cls, geminal_data: "Geminal_data") -> "Geminal_data": """Convert MOs to AOs.""" - if isinstance(geminal_data.orb_data_up_spin, AOs_sphe_data) and isinstance( - geminal_data.orb_data_dn_spin, AOs_sphe_data - ): - return geminal_data - elif isinstance(geminal_data.orb_data_up_spin, AOs_cart_data) and isinstance( - geminal_data.orb_data_dn_spin, AOs_cart_data + if ( + isinstance(geminal_data.orb_data_up_spin, AOs_sphe_data) + and isinstance(geminal_data.orb_data_dn_spin, AOs_sphe_data) + ) or ( + isinstance(geminal_data.orb_data_up_spin, AOs_cart_data) + and isinstance(geminal_data.orb_data_dn_spin, AOs_cart_data) ): return geminal_data - elif isinstance(geminal_data.orb_data_up_spin, MOs_data) and isinstance(geminal_data.orb_data_dn_spin, MOs_data): + if isinstance(geminal_data.orb_data_up_spin, MOs_data) and isinstance(geminal_data.orb_data_dn_spin, MOs_data): # split mo_lambda_matrix mo_lambda_matrix_paired, mo_lambda_matrix_unpaired = np.hsplit( geminal_data.lambda_matrix, [geminal_data.orb_num_dn] @@ -718,8 +706,7 @@ def convert_from_MOs_to_AOs(cls, geminal_data: "Geminal_data") -> "Geminal_data" aos_data_dn_spin, aos_lambda_matrix, ) - else: - raise NotImplementedError + raise NotImplementedError @classmethod def convert_from_AOs_to_MOs( diff --git a/jqmc/hamiltonians.py b/jqmc/hamiltonians.py index 1d88a144..27876be7 100644 --- a/jqmc/hamiltonians.py +++ b/jqmc/hamiltonians.py @@ -441,7 +441,7 @@ def _load_item(item: h5py.Group | h5py.Dataset | Any) -> Any: elif val.dtype.kind == "O" and val.size > 0 and isinstance(val.flat[0], bytes): val = np.array([v.decode("utf-8") for v in val.flat]).reshape(val.shape) return val - elif isinstance(item, h5py.Group): + if isinstance(item, h5py.Group): if item.attrs.get("_is_list"): lst = [] # Combine keys from subgroups/datasets and attributes @@ -457,25 +457,23 @@ def _load_item(item: h5py.Group | h5py.Dataset | Any) -> Any: elif k in item.attrs: lst.append(item.attrs[k]) return lst - elif item.attrs.get("_is_dict"): + if item.attrs.get("_is_dict"): d = {} for k in item.keys(): d[k] = _load_item(item[k]) return d - else: - # Dataclass or generic group - class_name = item.attrs.get("_class_name") - module_name = item.attrs.get("_module_name") - if class_name and module_name: - module = importlib.import_module(module_name) - sub_cls = getattr(module, class_name) - return _load_dataclass_from_hdf5(sub_cls, item) - else: - # Fallback for dicts saved without _is_dict or unknown structures - d = {} - for k in item.keys(): - d[k] = _load_item(item[k]) - return d + # Dataclass or generic group + class_name = item.attrs.get("_class_name") + module_name = item.attrs.get("_module_name") + if class_name and module_name: + module = importlib.import_module(module_name) + sub_cls = getattr(module, class_name) + return _load_dataclass_from_hdf5(sub_cls, item) + # Fallback for dicts saved without _is_dict or unknown structures + d = {} + for k in item.keys(): + d[k] = _load_item(item[k]) + return d return item diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 46fc8831..0a9e2589 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -114,7 +114,7 @@ def _ensure_flax_trace_level_compat() -> None: # Mark as patched to prevent repeated checks; do not mutate further when the # attribute exists but already works. - setattr(trace_level, "_jqmc_patched", True) + trace_level._jqmc_patched = True def _flatten_params_with_treedef(params: Any) -> tuple[jnp.ndarray, Any, list[tuple[int, ...]]]: @@ -1392,14 +1392,11 @@ def compute_orb_api(self) -> Callable[..., npt.NDArray[np.float64]]: NotImplementedError: If the instances of orb_data is neither AOs_data nor MOs_data. """ - if isinstance(self.orb_data, AOs_sphe_data): - return compute_AOs - elif isinstance(self.orb_data, AOs_cart_data): + if isinstance(self.orb_data, AOs_sphe_data) or isinstance(self.orb_data, AOs_cart_data): return compute_AOs - elif isinstance(self.orb_data, MOs_data): + if isinstance(self.orb_data, MOs_data): return compute_MOs - else: - raise NotImplementedError + raise NotImplementedError @property def _j_matrix_jnp(self) -> jax.Array: @@ -1413,40 +1410,36 @@ def ao_exponents(self) -> jax.Array: """AO Gaussian exponents (jnp view of underlying numpy storage).""" if isinstance(self.orb_data, (AOs_sphe_data, AOs_cart_data)): return self.orb_data._exponents_jnp - elif isinstance(self.orb_data, MOs_data): + if isinstance(self.orb_data, MOs_data): return self.orb_data.aos_data._exponents_jnp - else: - raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") + raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") @property def ao_coefficients(self) -> jax.Array: """AO contraction coefficients (jnp view of underlying numpy storage).""" if isinstance(self.orb_data, (AOs_sphe_data, AOs_cart_data)): return self.orb_data._coefficients_jnp - elif isinstance(self.orb_data, MOs_data): + if isinstance(self.orb_data, MOs_data): return self.orb_data.aos_data._coefficients_jnp - else: - raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") + raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") def with_updated_ao_exponents(self, new_exp: npt.NDArray[np.float64]) -> "Jastrow_three_body_data": """Return a new instance with updated AO exponents.""" if isinstance(self.orb_data, (AOs_sphe_data, AOs_cart_data)): return self.replace(orb_data=self.orb_data.replace(exponents=new_exp)) - elif isinstance(self.orb_data, MOs_data): + if isinstance(self.orb_data, MOs_data): new_aos = self.orb_data.aos_data.replace(exponents=new_exp) return self.replace(orb_data=self.orb_data.replace(aos_data=new_aos)) - else: - raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") + raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") def with_updated_ao_coefficients(self, new_coeff: npt.NDArray[np.float64]) -> "Jastrow_three_body_data": """Return a new instance with updated AO contraction coefficients.""" if isinstance(self.orb_data, (AOs_sphe_data, AOs_cart_data)): return self.replace(orb_data=self.orb_data.replace(coefficients=new_coeff)) - elif isinstance(self.orb_data, MOs_data): + if isinstance(self.orb_data, MOs_data): new_aos = self.orb_data.aos_data.replace(coefficients=new_coeff) return self.replace(orb_data=self.orb_data.replace(aos_data=new_aos)) - else: - raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") + raise NotImplementedError(f"Unsupported orb_data type: {type(self.orb_data)}") @classmethod def init_jastrow_three_body_data( diff --git a/jqmc/jqmc_cli.py b/jqmc/jqmc_cli.py index 75f1a982..f19d569a 100644 --- a/jqmc/jqmc_cli.py +++ b/jqmc/jqmc_cli.py @@ -93,15 +93,13 @@ def _cli(): """Main function.""" if len(sys.argv) == 1: raise ValueError("Please specify input toml file.") - elif len(sys.argv) > 2: + if len(sys.argv) > 2: raise ValueError("More than one input toml files are not acceptable.") - else: - toml_file = sys.argv[1] - if not os.path.isfile(toml_file): - raise FileNotFoundError(f"toml_file = {toml_file} does not exist.") - else: - with open(toml_file) as f: - dict_toml = toml.load(f) + toml_file = sys.argv[1] + if not os.path.isfile(toml_file): + raise FileNotFoundError(f"toml_file = {toml_file} does not exist.") + with open(toml_file) as f: + dict_toml = toml.load(f) # MPI related mpi_comm = MPI.COMM_WORLD diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index e7b4c906..d82dda4f 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -52,7 +52,6 @@ from jax import grad, jit, lax, vmap from jax import numpy as jnp from jax import typing as jnpt -from jax.scipy import linalg as jsp_linalg from mpi4py import MPI from ._diff_mask import DiffMask, apply_diff_mask @@ -3617,16 +3616,15 @@ def _compute_local_energy_t_debug( if tau_left <= 0.0: # '= is very important!!' jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) break - else: - # electron position update - # random choice - # k = np.random.choice(len(non_diagonal_move_probabilities), p=non_diagonal_move_probabilities) - jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) - cdf = jnp.cumsum(non_diagonal_move_probabilities) - random_value = jax.random.uniform(subkey, minval=0.0, maxval=1.0) - k = jnp.searchsorted(cdf, random_value) - r_up_carts = non_diagonal_move_mesh_r_up_carts[k] - r_dn_carts = non_diagonal_move_mesh_r_dn_carts[k] + # electron position update + # random choice + # k = np.random.choice(len(non_diagonal_move_probabilities), p=non_diagonal_move_probabilities) + jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) + cdf = jnp.cumsum(non_diagonal_move_probabilities) + random_value = jax.random.uniform(subkey, minval=0.0, maxval=1.0) + k = jnp.searchsorted(cdf, random_value) + r_up_carts = non_diagonal_move_mesh_r_up_carts[k] + r_dn_carts = non_diagonal_move_mesh_r_dn_carts[k] projection_counter_list[i_walker] = projection_counter e_L_list[i_walker] = e_L @@ -4800,7 +4798,6 @@ def _body_step_core( ``None`` (default) on the legacy path; the callees fall back to fresh AO evaluation when ``j3_state is None``. """ - # compute diagonal elements, kinetic part diagonal_kinetic_part = 3.0 / (2.0 * alat**2) * (len(r_up_carts) + len(r_dn_carts)) diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index 53da1415..586a777d 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -55,7 +55,6 @@ import toml from jax import grad, jit, lax, vmap from jax import numpy as jnp -from jax.scipy.linalg import lu_factor, lu_solve from mpi4py import MPI from ._diff_mask import DiffMask, apply_diff_mask @@ -1884,9 +1883,7 @@ def get_aH( _need_full_dE = return_matrices and g is not None and collective_obs is not None if _need_full_dE: pass # keep full dE_matrix; will slice after computing dE_SR - elif _cpi_for_dln is not None: - dE_matrix = dE_matrix[:, :, chosen_param_index] - elif chosen_param_index is not None and not (return_matrices and g is not None): + elif _cpi_for_dln is not None or (chosen_param_index is not None and not (return_matrices and g is not None)): dE_matrix = dE_matrix[:, :, chosen_param_index] # else: LM fallback (no collective_obs) -- keep full dE_matrix, slice later @@ -1917,7 +1914,7 @@ def get_aH( assert collective_obs.shape == (N,), f"collective_obs shape {collective_obs.shape} != ({N},)" assert K_dE >= K, f"dE must be full when collective_obs is used: K_dE={K_dE} < K={K}" else: - assert K == K_dE, f"O and dE dimension mismatch: K={K} != K_dE={K_dE}" + assert K_dE == K, f"O and dE dimension mismatch: K={K} != K_dE={K_dE}" if g is not None: assert g.shape[0] == K_dE, f"g dimension {g.shape[0]} != K_dE={K_dE}" diff --git a/jqmc/jqmc_tool.py b/jqmc/jqmc_tool.py index 23bd3fbe..b8adf2f7 100644 --- a/jqmc/jqmc_tool.py +++ b/jqmc/jqmc_tool.py @@ -93,7 +93,6 @@ @click.group() def _cli(): """The jQMC tools.""" - pass # trexio_app @@ -589,7 +588,7 @@ def hamiltonian_convert_wavefunction( if convert_to == "jsd": raise NotImplementedError("Conversion to JSD is not implemented yet.") - elif convert_to == "jagp": + if convert_to == "jagp": # conversion of SD to AGP typer.echo("Convert SD to AGP.") geminal_data = Geminal_data.convert_from_MOs_to_AOs(geminal_data) diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index b10c7825..4a291b96 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -57,7 +57,6 @@ from flax import struct from jax import jit -from ._jqmc_utility import _cart_to_spherical_matrix, _spherical_to_cart_matrix # myqmc module from ._precision import get_dtype_jnp diff --git a/jqmc/trexio_wrapper.py b/jqmc/trexio_wrapper.py index fbb34069..2cabb8f7 100644 --- a/jqmc/trexio_wrapper.py +++ b/jqmc/trexio_wrapper.py @@ -117,9 +117,8 @@ def read_trexio_file( # cell_c = trexio.read_cell_c(file_r) # k_point = trexio.read_pbc_k_point(file_r) raise NotImplementedError - else: - pbc_flag = False - # logger.info("Molecule (Open boundary condition)") + pbc_flag = False + # logger.info("Molecule (Open boundary condition)") logger.info(f"pbc_flag = {pbc_flag}") # read electron num @@ -183,8 +182,6 @@ def read_trexio_file( # complex_flag = True logger.error("Complex WFs are not supported.") raise NotImplementedError - else: - pass # logger.info("The WF is real") # complex_flag = False diff --git a/jqmc/wavefunction.py b/jqmc/wavefunction.py index 2d5fc6fb..58661bf1 100644 --- a/jqmc/wavefunction.py +++ b/jqmc/wavefunction.py @@ -80,9 +80,7 @@ _init_grads_laplacian_Jastrow_one_body_streaming_state, _init_grads_laplacian_Jastrow_three_body_streaming_state, _init_grads_laplacian_Jastrow_two_body_streaming_state, - compute_grads_and_laplacian_Jastrow_one_body, compute_grads_and_laplacian_Jastrow_part, - compute_grads_and_laplacian_Jastrow_two_body, compute_Jastrow_part, ) from .molecular_orbital import MOs_data diff --git a/jqmc_workflow/_cli.py b/jqmc_workflow/_cli.py index 76607084..2bba4af3 100644 --- a/jqmc_workflow/_cli.py +++ b/jqmc_workflow/_cli.py @@ -49,7 +49,6 @@ import toml import typer -import yaml from ._config import get_config_dir, template_dir @@ -284,7 +283,7 @@ def check_job(self, job_id: int, server_machine_name: str): logger.info(f" Job {stored_job_id} is RUNNING on {server}.") else: logger.info(f" Job {stored_job_id} is NOT in the queue on {server}.") - logger.info(f" (it may have finished or been cancelled)") + logger.info(" (it may have finished or been cancelled)") finally: machine.ssh_close() except Exception as ex: diff --git a/jqmc_workflow/_error_estimator.py b/jqmc_workflow/_error_estimator.py index f72dc4ed..b6d9b957 100644 --- a/jqmc_workflow/_error_estimator.py +++ b/jqmc_workflow/_error_estimator.py @@ -87,7 +87,7 @@ def estimate_required_steps( Minimum number of steps to return (e.g. warmup + bin_blocks). Default 0 (no minimum). - Returns + Returns: ------- int Estimated number of steps for the production run. @@ -143,7 +143,7 @@ def estimate_additional_steps( min_additional : int Floor on additional steps to avoid trivially short runs. - Returns + Returns: ------- int Number of *additional* steps to run. @@ -171,7 +171,7 @@ def estimate_additional_steps( def suffixed_name(filename: str, index: int) -> str: """Insert an integer suffix before the file extension. - Examples + Examples: -------- >>> suffixed_name("input.toml", 0) 'input_0.toml' @@ -185,7 +185,7 @@ def suffixed_name(filename: str, index: int) -> str: def _format_duration(seconds: float) -> str: """Format a duration in seconds as a human-readable string. - Examples + Examples: -------- >>> _format_duration(90) '1m 30s' @@ -241,7 +241,7 @@ def parse_net_time(output_file: str) -> float | None: output_file : str Path to the jQMC stdout/stderr output file. - Returns + Returns: ------- float or None Net time in seconds, or *None* if the pattern is not found. diff --git a/jqmc_workflow/_input_generator.py b/jqmc_workflow/_input_generator.py index 7b2403ba..e3522453 100644 --- a/jqmc_workflow/_input_generator.py +++ b/jqmc_workflow/_input_generator.py @@ -57,7 +57,7 @@ def resolve_with_defaults(section_name: str, explicit_params: dict) -> dict: ``{param_name: value_or_None}``. *None* entries are replaced by the corresponding default from ``cli_parameters``. - Returns + Returns: ------- dict Resolved parameters with no *None* values (unless the default @@ -87,7 +87,7 @@ def get_default_parameters(job_type: str) -> dict: job_type : str One of ``"mcmc"``, ``"vmc"``, ``"lrdmc-bra"``, ``"lrdmc-tau"``. - Returns + Returns: ------- dict ``{"control": {...}, job_type: {...}}`` @@ -124,12 +124,12 @@ def generate_input_toml( with_comments : bool If True, insert inline comments from ``cli_parameters["*_comments"]``. - Returns + Returns: ------- str The absolute path of the written file. - Examples + Examples: -------- >>> generate_input_toml( ... "mcmc", @@ -201,16 +201,15 @@ def _toml_value(v) -> str: """Format a Python value as a TOML literal.""" if isinstance(v, bool): return "true" if v else "false" - elif isinstance(v, str): + if isinstance(v, str): return f'"{v}"' - elif isinstance(v, (int, float)): + if isinstance(v, (int, float)): return str(v) - elif isinstance(v, dict): + if isinstance(v, dict): # Inline table inner = ", ".join(f"{k} = {_toml_value(val)}" for k, val in v.items()) return "{" + inner + "}" - elif isinstance(v, list): + if isinstance(v, list): inner = ", ".join(_toml_value(item) for item in v) return f"[{inner}]" - else: - return repr(v) + return repr(v) diff --git a/jqmc_workflow/_job.py b/jqmc_workflow/_job.py index 7db523a1..ca1430f7 100644 --- a/jqmc_workflow/_job.py +++ b/jqmc_workflow/_job.py @@ -63,7 +63,7 @@ def load_queue_data(server_machine_name: str, queue_label: str) -> dict: queue_label : str Section key in ``queue_data.toml``. - Returns + Returns: ------- dict The TOML table for *queue_label*. @@ -393,7 +393,7 @@ def job_acct(self) -> tuple[str, str, str] | None: is performed -- the user specifies the complete command with flags in the config. - Returns + Returns: ------- tuple[str, str, str] | None ``(command, stdout, stderr)`` on success. diff --git a/jqmc_workflow/_lrdmc_calibration.py b/jqmc_workflow/_lrdmc_calibration.py index f3c7a1dd..0a9feba1 100644 --- a/jqmc_workflow/_lrdmc_calibration.py +++ b/jqmc_workflow/_lrdmc_calibration.py @@ -66,12 +66,12 @@ def get_num_electrons(hamiltonian_file: str) -> int: hamiltonian_file : str Path to ``hamiltonian_data.h5``. - Returns + Returns: ------- int Total electron count ``num_electron_up + num_electron_dn``. - Raises + Raises: ------ RuntimeError If the electron counts cannot be found in the file. @@ -103,7 +103,7 @@ def parse_survived_walkers_ratio(output_file: str) -> float | None: output_file : str Path to the jqmc stdout file. - Returns + Returns: ------- float or None Survived walkers ratio as a fraction, or *None* if not found. @@ -144,13 +144,13 @@ def fit_num_projection_per_measurement( target_ratio : float Target survived-walkers ratio (e.g. 0.97). - Returns + Returns: ------- int Optimal ``num_projection_per_measurement`` (rounded up to the nearest even integer, minimum 2). - Raises + Raises: ------ RuntimeError If the linear fit cannot determine a positive root. @@ -223,7 +223,7 @@ def scale_num_projection_per_measurement( alat : float Target lattice spacing (bohr). - Returns + Returns: ------- int Scaled ``num_projection_per_measurement`` (rounded up to nearest even diff --git a/jqmc_workflow/_machine.py b/jqmc_workflow/_machine.py index 92448380..a532bea6 100644 --- a/jqmc_workflow/_machine.py +++ b/jqmc_workflow/_machine.py @@ -342,8 +342,7 @@ def run_command(self, command: str, execute_dir: str = None): if self.machine_type == "local": return self._run_local(command_r) - else: - return self._run_remote(command_r) + return self._run_remote(command_r) def _run_local(self, command_r: str, max_retries: int = 10): for attempt in range(max_retries): diff --git a/jqmc_workflow/_output_parser.py b/jqmc_workflow/_output_parser.py index 36692b0c..5ec95774 100644 --- a/jqmc_workflow/_output_parser.py +++ b/jqmc_workflow/_output_parser.py @@ -61,7 +61,6 @@ import os import re from logging import getLogger -from typing import Optional import toml @@ -95,7 +94,7 @@ def parse_ufloat_short(text: str): A single token like ``"+0.0114(14)"``, ``"-1.23(4)"``, ``"+3(8)e-05"``, or ``"+3.9(3.5)e-05"``. - Returns + Returns: ------- tuple ``(value, uncertainty)`` or ``(None, None)`` on failure. @@ -142,7 +141,7 @@ def parse_force_table(text: str): text : str Full stdout from ``jqmc-tool {mcmc,lrdmc} compute-force``. - Returns + Returns: ------- list of dict or None Each dict: ``{label, Fx, Fx_err, Fy, Fy_err, Fz, Fz_err}``. @@ -543,7 +542,7 @@ def _parse_vmc_log_text(text: str) -> list: This function groups the data by optimization step. Lines before the first ``Optimization step`` header are ignored. - Returns + Returns: ------- list of VMC_Step_Data One entry per optimization step found. @@ -664,7 +663,7 @@ def parse_vmc_output(work_dir: str) -> VMC_Diagnostic_Data: work_dir : str Path to the VMC working directory. - Returns + Returns: ------- VMC_Diagnostic_Data Structured parse result containing per-step data and metadata. @@ -781,7 +780,7 @@ def parse_mcmc_output(work_dir: str) -> MCMC_Diagnostic_Data: work_dir : str Path to the MCMC working directory. - Returns + Returns: ------- MCMC_Diagnostic_Data Structured parse result. @@ -891,7 +890,7 @@ def parse_lrdmc_output(work_dir: str) -> LRDMC_Diagnostic_Data: work_dir : str Path to the LRDMC working directory. - Returns + Returns: ------- LRDMC_Diagnostic_Data Structured parse result. @@ -1004,7 +1003,7 @@ def parse_lrdmc_ext_output(work_dir: str) -> LRDMC_Ext_Diagnostic_Data: work_dir : str Path to the LRDMC extrapolation working directory. - Returns + Returns: ------- LRDMC_Ext_Diagnostic_Data Structured parse result. @@ -1083,7 +1082,7 @@ def parse_input_params(work_dir: str) -> Input_Parameters: work_dir : str Path to the workflow working directory. - Returns + Returns: ------- Input_Parameters Structured parameter data with per-input detail. diff --git a/jqmc_workflow/_results.py b/jqmc_workflow/_results.py index f0d732e8..752e08b7 100644 --- a/jqmc_workflow/_results.py +++ b/jqmc_workflow/_results.py @@ -56,7 +56,6 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Optional # -- VMC ----------------------------------------------------------- @@ -65,7 +64,7 @@ class VMC_Step_Data: """Data for one VMC optimization step. - Attributes + Attributes: ---------- step : int Optimization step number (``Optimization step = N/M`` -> N). @@ -112,7 +111,7 @@ class VMC_Step_Data: class VMC_Diagnostic_Data: """Aggregated parse result for an entire VMC optimization. - Attributes + Attributes: ---------- steps : list of VMC_Step_Data Per-step data in chronological order. @@ -165,7 +164,7 @@ class VMC_Diagnostic_Data: class MCMC_Diagnostic_Data: """Parse result for an MCMC sampling run. - Attributes + Attributes: ---------- acceptance_ratio : float or None ``Acceptance ratio is X %`` -> X / 100. @@ -230,7 +229,7 @@ class MCMC_Diagnostic_Data: class LRDMC_Diagnostic_Data: """Parse result for an LRDMC calculation. - Attributes + Attributes: ---------- survived_walkers_ratio : float or None ``Survived walkers ratio = X %`` -> X / 100. @@ -296,7 +295,7 @@ class LRDMC_Diagnostic_Data: class LRDMC_Ext_Diagnostic_Data: """Parse result for an LRDMC a^2->0 extrapolation. - Attributes + Attributes: ---------- extrapolated_energy : float or None ``For a -> 0 bohr: E = X +- Y Ha.`` -> X. @@ -335,7 +334,7 @@ class Input_Parameters: "": { ... all job-type params with defaults ... }, } - Attributes + Attributes: ---------- actual_opt_steps : int or None For VMC: last completed optimization step stored in diff --git a/jqmc_workflow/_state.py b/jqmc_workflow/_state.py index 4e633248..bfe7336a 100644 --- a/jqmc_workflow/_state.py +++ b/jqmc_workflow/_state.py @@ -243,7 +243,7 @@ def validate_completion( previously accepted ``error <= target * 1.05`` (MCMC) or ``error <= target * 1.20`` (LRDMC) pass the matching factor. - Returns + Returns: ------- status : CompletionStatus ``OK`` / ``FAILED`` / ``INCOMPLETE``. diff --git a/jqmc_workflow/launcher.py b/jqmc_workflow/launcher.py index 818b76ed..cb12757f 100644 --- a/jqmc_workflow/launcher.py +++ b/jqmc_workflow/launcher.py @@ -40,7 +40,6 @@ import asyncio import os import pathlib -from datetime import datetime from logging import ( FileHandler, Formatter, @@ -81,13 +80,13 @@ class Launcher: If ``True``, render the dependency graph to ``dependency_graph.png`` (requires the ``graphviz`` Python package). - Raises + Raises: ------ ValueError If workflow labels are duplicated or a dependency references an undefined workflow label. - Examples + Examples: -------- Typical three-stage QMC pipeline:: @@ -133,14 +132,14 @@ class Launcher: ) launcher.launch() - Notes + Notes: ----- * The launcher changes the working directory during execution and restores it afterwards. * If a workflow fails, all downstream dependents are automatically skipped. - See Also + See Also: -------- Container : Wraps a workflow in a project directory. FileFrom : File dependency placeholder. @@ -379,11 +378,10 @@ def _get_value(self, dep_obj): p = pathlib.Path(filepath) return p.resolve().relative_to(pathlib.Path(self.root_dir).resolve()) - elif isinstance(dep_obj, ValueFrom): + if isinstance(dep_obj, ValueFrom): return cw.output_values.get(dep_obj.key) - else: - raise ValueError(f"Unknown dependency type: {dep_obj}") + raise ValueError(f"Unknown dependency type: {dep_obj}") def _resolve_variables(self, cw: Container): """Replace all dependency placeholders in cw with resolved values.""" @@ -464,8 +462,7 @@ async def async_launch(self): if pending: logger.error(f"Deadlock! Remaining: {pending}") break - else: - break + break # Wait for at least one task to complete done_tasks, _ = await asyncio.wait(running.values(), return_when=asyncio.FIRST_COMPLETED) diff --git a/jqmc_workflow/lrdmc_ext_workflow.py b/jqmc_workflow/lrdmc_ext_workflow.py index d4e89f1b..faed141b 100644 --- a/jqmc_workflow/lrdmc_ext_workflow.py +++ b/jqmc_workflow/lrdmc_ext_workflow.py @@ -163,7 +163,7 @@ class LRDMC_Ext_Workflow(Workflow): workflow targets a remote machine. Passed through to each child :class:`LRDMC_Workflow`. Default *None* (no cleanup). - Examples + Examples: -------- GFMC_t mode (default):: @@ -215,14 +215,14 @@ class LRDMC_Ext_Workflow(Workflow): error : str Top-level error message (only on failure). - Notes + Notes: ----- * At least two ``alat`` values are required for extrapolation. With a single value, per-alat results are returned but no extrapolation is performed. * Each sub-run directory is named ``lrdmc_alat_/``. - See Also + See Also: -------- LRDMC_Workflow : Single-alat LRDMC run. """ @@ -328,7 +328,7 @@ def _make_lrdmc_workflow(self, alat): alat : float Lattice spacing. - Returns + Returns: ------- Container """ @@ -400,7 +400,7 @@ async def run(self) -> tuple: calibration (``_pilot_a``), error-bar pilot (``_pilot_b``), and production phase. - Returns + Returns: ------- tuple ``(status, output_files, output_values)`` @@ -490,7 +490,7 @@ async def _run_one(enc): def _extrapolate_energy(self, restart_chks: list[str]): """Run ``jqmc-tool lrdmc extrapolate-energy``. - Returns + Returns: ------- tuple ``(energy, error)`` or ``(None, None)``. diff --git a/jqmc_workflow/lrdmc_workflow.py b/jqmc_workflow/lrdmc_workflow.py index 0ebb5f97..bf3dc366 100644 --- a/jqmc_workflow/lrdmc_workflow.py +++ b/jqmc_workflow/lrdmc_workflow.py @@ -54,7 +54,6 @@ import subprocess import time from logging import getLogger -from typing import Optional from ._error_estimator import ( _format_duration, @@ -89,7 +88,7 @@ class LRDMC_Workflow(Workflow): - """Single LRDMC (Lattice-Regularized Diffusion Monte Carlo) run. + r"""Single LRDMC (Lattice-Regularized Diffusion Monte Carlo) run. Generates a ``job_type=lrdmc-bra`` (GFMC_n) or ``job_type=lrdmc-tau`` (GFMC_t) input TOML at a fixed lattice spacing ``alat``, submits @@ -210,7 +209,7 @@ class LRDMC_Workflow(Workflow): are always removed; remote files are removed only when the workflow targets a remote machine. Default *None* (no cleanup). - Examples + Examples: -------- GFMC_t mode (default):: @@ -278,14 +277,14 @@ class LRDMC_Workflow(Workflow): time_projection_tau : float Imaginary-time projection step (GFMC_t mode only). - Notes + Notes: ----- * For a^2->0 continuum-limit extrapolation, use :class:`LRDMC_Ext_Workflow` instead. * The pilot is skipped on re-entrance if an estimation already exists in ``workflow_state.toml``. - See Also + See Also: -------- LRDMC_Ext_Workflow : Multi-alat extrapolation wrapper. MCMC_Workflow : VMC production sampling (job_type=mcmc). @@ -553,7 +552,7 @@ async def _launch_fixed_steps(self, _wd): logger.info(f" step {i}: already fetched. Skipping.") last_run = i continue - elif status_i in ("submitted", "completed"): + if status_i in ("submitted", "completed"): input_i = recorded["input_file"] output_i = recorded["output_file"] run_id_i = recorded.get("run_id", "") @@ -1224,7 +1223,7 @@ def _compute_energy(self, restart_chk: str, work_dir: str, output_file: str | No output_file : str, optional Stdout filename (basename) of the ``jqmc`` run. - Returns + Returns: ------- tuple ``(energy, error)`` or ``(None, None)``. @@ -1290,7 +1289,7 @@ def _compute_force(self, restart_chk: str, work_dir: str, output_file: str | Non output_file : str, optional Stdout filename (basename) of the ``jqmc`` run. - Returns + Returns: ------- list of dict or None Each dict has keys ``label``, ``Fx``, ``Fx_err``, diff --git a/jqmc_workflow/mcmc_workflow.py b/jqmc_workflow/mcmc_workflow.py index 0b088c03..6337f44e 100644 --- a/jqmc_workflow/mcmc_workflow.py +++ b/jqmc_workflow/mcmc_workflow.py @@ -43,7 +43,6 @@ import subprocess import time from logging import getLogger -from typing import Optional from ._error_estimator import ( _format_duration, @@ -154,7 +153,7 @@ class MCMC_Workflow(Workflow): are always removed; remote files are removed only when the workflow targets a remote machine. Default *None* (no cleanup). - Examples + Examples: -------- Standalone launch (automatic mode):: @@ -206,14 +205,14 @@ class MCMC_Workflow(Workflow): Estimated total measurement steps (automatic mode). In fixed-step mode this key is ``estimated_steps``. - Notes + Notes: ----- * The pilot run is skipped on re-entrance if an estimation already exists in ``workflow_state.toml``. * Continuation runs restart from the most recent ``.h5`` checkpoint file. - See Also + See Also: -------- VMC_Workflow : Wavefunction optimisation (job_type=vmc). LRDMC_Workflow : Diffusion Monte Carlo (job_type=lrdmc-bra / lrdmc-tau). @@ -392,7 +391,7 @@ async def _launch_fixed_steps(self, _wd): step_files[i] = (recorded["input_file"], recorded["output_file"], recorded.get("run_id", "")) last_run = i continue - elif status in ("submitted", "completed"): + if status in ("submitted", "completed"): input_i = recorded["input_file"] output_i = recorded["output_file"] run_id_i = recorded.get("run_id", "") @@ -917,7 +916,7 @@ def _compute_energy(self, restart_chk: str, work_dir: str, output_file: str | No output_file : str, optional Stdout filename (basename) of the ``jqmc`` run. - Returns + Returns: ------- tuple ``(energy, error)`` or ``(None, None)``. @@ -978,7 +977,7 @@ def _compute_force(self, restart_chk: str, work_dir: str, output_file: str | Non output_file : str, optional Stdout filename (basename) of the ``jqmc`` run. - Returns + Returns: ------- list of dict or None Each dict has keys ``label``, ``Fx``, ``Fx_err``, diff --git a/jqmc_workflow/vmc_workflow.py b/jqmc_workflow/vmc_workflow.py index 489faf7a..495a43b1 100644 --- a/jqmc_workflow/vmc_workflow.py +++ b/jqmc_workflow/vmc_workflow.py @@ -42,7 +42,6 @@ import re import time from logging import getLogger -from typing import Optional from ._error_estimator import ( _format_duration, @@ -65,7 +64,7 @@ class VMC_Workflow(Workflow): - """VMC (Variational Monte Carlo) Jastrow / orbital optimisation workflow. + r"""VMC (Variational Monte Carlo) Jastrow / orbital optimisation workflow. Generates a ``job_type=vmc`` input TOML, submits ``jqmc``, monitors until completion, and collects the optimised @@ -184,7 +183,7 @@ class VMC_Workflow(Workflow): are always removed; remote files are removed only when the workflow targets a remote machine. Default *None* (no cleanup). - Examples + Examples: -------- Standalone launch (automatic mode):: @@ -253,7 +252,7 @@ class VMC_Workflow(Workflow): energy_slope_std : float Standard deviation of the energy slope. - Notes + Notes: ----- * The pilot uses a small number of opt steps (``pilot_vmc_steps``) just to estimate the error. The real optimisation happens in @@ -261,7 +260,7 @@ class VMC_Workflow(Workflow): * The estimation is stored in ``workflow_state.toml`` under ``[estimation]``; on re-entrance the pilot is skipped. - See Also + See Also: -------- MCMC_Workflow : VMC production sampling (job_type=mcmc). LRDMC_Workflow : Diffusion Monte Carlo (job_type=lrdmc-bra / lrdmc-tau). @@ -506,7 +505,7 @@ async def _launch_fixed_steps(self, _wd): step_files[i] = (recorded["input_file"], recorded["output_file"], recorded.get("run_id", "")) last_run = i continue - elif status in ("submitted", "completed"): + if status in ("submitted", "completed"): input_i = recorded["input_file"] output_i = recorded["output_file"] run_id_i = recorded.get("run_id", "") @@ -736,7 +735,7 @@ async def _launch_auto(self, _wd): step_files[i] = (recorded["input_file"], recorded["output_file"], recorded.get("run_id", "")) last_run = i continue - elif status in ("submitted", "completed"): + if status in ("submitted", "completed"): input_i = recorded["input_file"] output_i = recorded["output_file"] run_id_i = recorded.get("run_id", "") @@ -995,7 +994,7 @@ def _parse_output(self, output_file=None): def _parse_all_snr(output_file): """Parse all signal-to-noise ratios from a VMC output file. - Returns + Returns: ------- list[float] All ``max(|f|/|std f|)`` values in order, one per @@ -1024,7 +1023,7 @@ def _parse_all_energies(output_file: str) -> list[tuple[float, float]]: Uses the existing ``_parse_vmc_log_text()`` parser to obtain :class:`VMC_Step_Data` and returns the energy/error pairs. - Returns + Returns: ------- list[tuple[float, float]] ``[(E_1, sigma_1), (E_2, sigma_2), ...]`` in file order. @@ -1058,7 +1057,7 @@ def _fit_energy_slope( energy_errors : list[float] Statistical error per step (length *N*, positive). - Returns + Returns: ------- slope : float Weighted least-squares slope *b*. @@ -1090,7 +1089,7 @@ def _parse_last_opt_energy(output_file): Extracts the energy from the *last* optimization step, which reflects the optimized wavefunction quality. - Returns + Returns: ------- tuple ``(energy, error)`` or ``(None, None)``. diff --git a/jqmc_workflow/wf_workflow.py b/jqmc_workflow/wf_workflow.py index d400cf99..41779883 100644 --- a/jqmc_workflow/wf_workflow.py +++ b/jqmc_workflow/wf_workflow.py @@ -85,7 +85,7 @@ class WF_Workflow(Workflow): ``"sphe"`` -> convert to spherical-harmonic AOs, ``None`` -> keep the original representation. - Example + Example: ------- >>> wf = WF_Workflow( ... trexio_file="molecular.h5", @@ -97,13 +97,13 @@ class WF_Workflow(Workflow): ... ) >>> status, out_files, out_values = wf.launch() - Notes + Notes: ----- This workflow runs **locally** -- no remote job submission is involved. It calls ``jqmc-tool trexio convert-to`` via :func:`subprocess.run`. - See Also + See Also: -------- VMC_Workflow : Optimise the wavefunction produced by this step. """ @@ -175,7 +175,7 @@ def configure(self) -> dict: async def run(self) -> tuple: """Run the TREXIO->hamiltonian conversion (locally). - Returns + Returns: ------- tuple ``(status, output_files, output_values)`` diff --git a/jqmc_workflow/workflow.py b/jqmc_workflow/workflow.py index 6a4714b9..f040a6f6 100644 --- a/jqmc_workflow/workflow.py +++ b/jqmc_workflow/workflow.py @@ -90,7 +90,7 @@ class FileFrom: that are only determined at runtime (e.g. the optimised Hamiltonian whose step number depends on convergence). - Examples + Examples: -------- Static filename (step number known in advance):: @@ -115,7 +115,7 @@ class FileFrom: workflow=MCMC_Workflow(...), ) - See Also + See Also: -------- ValueFrom : Declare a scalar-value dependency. Launcher : Resolves ``FileFrom`` / ``ValueFrom`` at launch time. @@ -154,7 +154,7 @@ class ValueFrom: * :class:`LRDMC_Ext_Workflow` -- ``extrapolated_energy``, ``extrapolated_energy_error``, ``per_alat_results``, ... - Examples + Examples: -------- Feed the MCMC energy into an LRDMC workflow as ``trial_energy``:: @@ -167,7 +167,7 @@ class ValueFrom: FileFrom("vmc-opt", ValueFrom("vmc-opt", "optimized_hamiltonian")) - See Also + See Also: -------- FileFrom : Declare a file dependency. Launcher : Resolves ``FileFrom`` / ``ValueFrom`` at launch time. @@ -212,7 +212,7 @@ class Workflow: files are removed only when the workflow targets a remote machine. Default is *None* (empty list -- no cleanup). - Attributes + Attributes: ---------- status : WorkflowStatus Current lifecycle status. @@ -227,7 +227,7 @@ class Workflow: cleanup_patterns : list[str] Glob patterns for post-completion file cleanup. - Notes + Notes: ----- **Subclass contract:** @@ -235,7 +235,7 @@ class Workflow: ``(status, output_files, output_values)`` from ``run()``. * Call ``super().__init__()`` in your constructor. - Examples + Examples: -------- Minimal custom workflow:: @@ -370,12 +370,12 @@ async def async_submit(self, action: str = "run") -> dict: MCP tool name (e.g. ``"run_vmc"``). Checked against :func:`allowed_actions` for the current phase and status. - Returns + Returns: ------- dict ``{"status": "submitted", "project_dir": ...}``. - Raises + Raises: ------ ValueError If *action* is not allowed in the current phase/status. @@ -393,7 +393,7 @@ async def async_submit(self, action: str = "run") -> dict: async def async_poll(self) -> dict: """Check whether the submitted workflow has completed. - Returns + Returns: ------- dict Status dict. Includes ``get_workflow_summary()`` when @@ -411,12 +411,12 @@ async def async_poll(self) -> dict: async def async_collect(self) -> dict: """Collect results from the completed workflow. - Returns + Returns: ------- dict ``{"status": ..., "output_files": [...], **output_values}``. - Raises + Raises: ------ RuntimeError If the workflow was not submitted or is still running. @@ -660,7 +660,7 @@ class Container: workflow : Workflow The inner :class:`Workflow` instance to execute. - Attributes + Attributes: ---------- output_files : list[str] Output filenames (populated after launch). @@ -671,7 +671,7 @@ class Container: project_dir : str Absolute path to the project directory. - Examples + Examples: -------- Wrap a VMC optimization in its own directory:: @@ -687,7 +687,7 @@ class Container: ) status, files, values = enc.launch() - See Also + See Also: -------- Launcher : Execute multiple ``Container`` objects as a DAG. FileFrom : Reference an output file from another workflow. @@ -721,7 +721,6 @@ def __init__( def _prepare(self): """Create project dir, copy input files, write initial state.""" - state = read_state(self.project_dir) existing_status = state.get("workflow", {}).get("status", "") @@ -760,7 +759,7 @@ def _dst_basename(src: str, rename_list: list, index: int) -> str: def _copy_input_files(self): """Copy input files into the project directory. - Raises + Raises: ------ FileNotFoundError If a required input file or directory does not exist. @@ -818,7 +817,7 @@ def _validate_input_files(self, proj: str): are **not** validated here because some workflows (e.g. ``WF_Workflow``) *produce* them rather than consume them. - Raises + Raises: ------ FileNotFoundError With a clear message listing all missing files, raised @@ -980,7 +979,7 @@ async def async_submit(self, action: str = "run") -> dict: action : str MCP tool name for action validation. - Returns + Returns: ------- dict ``{"status": "submitted", "label": ..., "project_dir": ...}``. @@ -997,7 +996,7 @@ async def async_submit(self, action: str = "run") -> dict: async def async_poll(self) -> dict: """Check whether the container's workflow has completed. - Returns + Returns: ------- dict Status dict with ``get_workflow_summary()`` when running. @@ -1014,13 +1013,13 @@ async def async_poll(self) -> dict: async def async_collect(self) -> dict: """Collect results from the completed container workflow. - Returns + Returns: ------- dict ``{"status": ..., "label": ..., "output_files": [...], **output_values}``. - Raises + Raises: ------ RuntimeError If not submitted or still running. diff --git a/pyproject.toml b/pyproject.toml index 1961674b..8f7678c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,37 +38,32 @@ lint.extend-ignore = [ "SIM108", # ternary if-else (sometimes if-block is more readable) # --- TODO: currently-violating rules; fix and remove from this list --- # bugbear - "B007", "B010", "B904", "B905", + "B007", "B904", "B905", # flake8-comprehensions - "C401", "C408", "C409", "C414", "C416", "C419", "C420", + "C401", "C408", "C409", "C414", "C416", "C419", # pydocstyle - "D101", "D102", "D103", "D104", "D105", "D107", "D200", "D202", - "D205", "D209", "D210", "D212", "D301", "D403", "D415", "D416", + "D101", "D102", "D103", "D104", "D105", "D107", + "D205", "D301", "D415", # pycodestyle errors - "E101", "E401", "E402", "E701", "E702", "E721", "E902", + "E101", "E402", "E701", "E702", "E721", "E902", # pyflakes - "F401", "F541", "F811", "F841", + "F841", # logging-format "G003", # numpy "NPY002", # performance "PERF401", "PERF402", - # flake8-pie - "PIE790", "PIE808", # flake8-pytest-style "PT006", "PT012", "PT018", # flake8-return - "RET501", "RET503", "RET504", "RET505", "RET506", "RET507", "RET508", + "RET503", "RET504", # ruff-specific (RUF001/002/003 are ENFORCED -- never add here) - "RUF005", "RUF010", "RUF019", "RUF021", "RUF022", "RUF023", - "RUF046", "RUF059", + "RUF005", "RUF022", "RUF046", "RUF059", # flake8-simplify - "SIM101", "SIM102", "SIM103", "SIM105", "SIM114", "SIM115", "SIM118", "SIM300", + "SIM101", "SIM102", "SIM103", "SIM105", "SIM115", "SIM118", # flake8-print "T201", - # pycodestyle warnings - "W291", "W293", "W605", ] exclude = ["jqmc/obsolete", "prototypes"] diff --git a/tests/test_AOs.py b/tests/test_AOs.py index 292dd338..d074f6d8 100755 --- a/tests/test_AOs.py +++ b/tests/test_AOs.py @@ -96,100 +96,100 @@ def Y_l_m_ref(l=0, m=0, r_cart_rel=None): if (l, m) == (0, 0): return 1.0 / 2.0 * np.sqrt(1.0 / np.pi) * r**0.0 # p orbitals - elif (l, m) == (1, -1): + if (l, m) == (1, -1): return np.sqrt(3.0 / (4 * np.pi)) * y / r - elif (l, m) == (1, 0): + if (l, m) == (1, 0): return np.sqrt(3.0 / (4 * np.pi)) * z / r - elif (l, m) == (1, 1): + if (l, m) == (1, 1): return np.sqrt(3.0 / (4 * np.pi)) * x / r # d orbitals - elif (l, m) == (2, -2): + if (l, m) == (2, -2): return 1.0 / 2.0 * np.sqrt(15.0 / (np.pi)) * x * y / r**2 - elif (l, m) == (2, -1): + if (l, m) == (2, -1): return 1.0 / 2.0 * np.sqrt(15.0 / (np.pi)) * y * z / r**2 - elif (l, m) == (2, 0): + if (l, m) == (2, 0): return 1.0 / 4.0 * np.sqrt(5.0 / (np.pi)) * (3 * z**2 - r**2) / r**2 - elif (l, m) == (2, 1): + if (l, m) == (2, 1): return 1.0 / 2.0 * np.sqrt(15.0 / (np.pi)) * x * z / r**2 - elif (l, m) == (2, 2): + if (l, m) == (2, 2): return 1.0 / 4.0 * np.sqrt(15.0 / (np.pi)) * (x**2 - y**2) / r**2 # f orbitals - elif (l, m) == (3, -3): + if (l, m) == (3, -3): return 1.0 / 4.0 * np.sqrt(35.0 / (2 * np.pi)) * y * (3 * x**2 - y**2) / r**3 - elif (l, m) == (3, -2): + if (l, m) == (3, -2): return 1.0 / 2.0 * np.sqrt(105.0 / (np.pi)) * x * y * z / r**3 - elif (l, m) == (3, -1): + if (l, m) == (3, -1): return 1.0 / 4.0 * np.sqrt(21.0 / (2 * np.pi)) * y * (5 * z**2 - r**2) / r**3 - elif (l, m) == (3, 0): + if (l, m) == (3, 0): return 1.0 / 4.0 * np.sqrt(7.0 / (np.pi)) * (5 * z**3 - 3 * z * r**2) / r**3 - elif (l, m) == (3, 1): + if (l, m) == (3, 1): return 1.0 / 4.0 * np.sqrt(21.0 / (2 * np.pi)) * x * (5 * z**2 - r**2) / r**3 - elif (l, m) == (3, 2): + if (l, m) == (3, 2): return 1.0 / 4.0 * np.sqrt(105.0 / (np.pi)) * (x**2 - y**2) * z / r**3 - elif (l, m) == (3, 3): + if (l, m) == (3, 3): return 1.0 / 4.0 * np.sqrt(35.0 / (2 * np.pi)) * x * (x**2 - 3 * y**2) / r**3 # g orbitals - elif (l, m) == (4, -4): + if (l, m) == (4, -4): return 3.0 / 4.0 * np.sqrt(35.0 / (np.pi)) * x * y * (x**2 - y**2) / r**4 - elif (l, m) == (4, -3): + if (l, m) == (4, -3): return 3.0 / 4.0 * np.sqrt(35.0 / (2 * np.pi)) * y * z * (3 * x**2 - y**2) / r**4 - elif (l, m) == (4, -2): + if (l, m) == (4, -2): return 3.0 / 4.0 * np.sqrt(5.0 / (np.pi)) * x * y * (7 * z**2 - r**2) / r**4 - elif (l, m) == (4, -1): + if (l, m) == (4, -1): return 3.0 / 4.0 * np.sqrt(5.0 / (2 * np.pi)) * y * (7 * z**3 - 3 * z * r**2) / r**4 - elif (l, m) == (4, 0): + if (l, m) == (4, 0): return 3.0 / 16.0 * np.sqrt(1.0 / (np.pi)) * (35 * z**4 - 30 * z**2 * r**2 + 3 * r**4) / r**4 - elif (l, m) == (4, 1): + if (l, m) == (4, 1): return 3.0 / 4.0 * np.sqrt(5.0 / (2 * np.pi)) * x * (7 * z**3 - 3 * z * r**2) / r**4 - elif (l, m) == (4, 2): + if (l, m) == (4, 2): return 3.0 / 8.0 * np.sqrt(5.0 / (np.pi)) * (x**2 - y**2) * (7 * z**2 - r**2) / r**4 - elif (l, m) == (4, 3): + if (l, m) == (4, 3): return 3.0 / 4.0 * np.sqrt(35.0 / (2 * np.pi)) * x * z * (x**2 - 3 * y**2) / r**4 - elif (l, m) == (4, 4): + if (l, m) == (4, 4): return 3.0 / 16.0 * np.sqrt(35.0 / (np.pi)) * (x**2 * (x**2 - 3 * y**2) - y**2 * (3 * x**2 - y**2)) / r**4 - elif (l, m) == (5, -5): + if (l, m) == (5, -5): return 3.0 / 16.0 * np.sqrt(77.0 / (2 * np.pi)) * (5 * x**4 * y - 10 * x**2 * y**3 + y**5) / r**5 - elif (l, m) == (5, -4): + if (l, m) == (5, -4): return 3.0 / 16.0 * np.sqrt(385.0 / np.pi) * 4 * x * y * z * (x**2 - y**2) / r**5 - elif (l, m) == (5, -3): + if (l, m) == (5, -3): return 1.0 / 16.0 * np.sqrt(385.0 / (2 * np.pi)) * -1 * (y**3 - 3 * x**2 * y) * (9 * z**2 - r**2) / r**5 - elif (l, m) == (5, -2): + if (l, m) == (5, -2): return 1.0 / 8.0 * np.sqrt(1155 / np.pi) * 2 * x * y * (3 * z**3 - z * r**2) / r**5 - elif (l, m) == (5, -1): + if (l, m) == (5, -1): return 1.0 / 16.0 * np.sqrt(165 / np.pi) * y * (21 * z**4 - 14 * z**2 * r**2 + r**4) / r**5 - elif (l, m) == (5, 0): + if (l, m) == (5, 0): return 1.0 / 16.0 * np.sqrt(11 / np.pi) * (63 * z**5 - 70 * z**3 * r**2 + 15 * z * r**4) / r**5 - elif (l, m) == (5, 1): + if (l, m) == (5, 1): return 1.0 / 16.0 * np.sqrt(165 / np.pi) * x * (21 * z**4 - 14 * z**2 * r**2 + r**4) / r**5 - elif (l, m) == (5, 2): + if (l, m) == (5, 2): return 1.0 / 8.0 * np.sqrt(1155 / np.pi) * (x**2 - y**2) * (3 * z**3 - z * r**2) / r**5 - elif (l, m) == (5, 3): + if (l, m) == (5, 3): return 1.0 / 16.0 * np.sqrt(385.0 / (2 * np.pi)) * (x**3 - 3 * x * y**2) * (9 * z**2 - r**2) / r**5 - elif (l, m) == (5, 4): + if (l, m) == (5, 4): return 3.0 / 16.0 * np.sqrt(385.0 / np.pi) * (x**2 * z * (x**2 - 3 * y**2) - y**2 * z * (3 * x**2 - y**2)) / r**5 - elif (l, m) == (5, 5): + if (l, m) == (5, 5): return 3.0 / 16.0 * np.sqrt(77.0 / (2 * np.pi)) * (x**5 - 10 * x**3 * y**2 + 5 * x * y**4) / r**5 - elif (l, m) == (6, -6): + if (l, m) == (6, -6): return 1.0 / 64.0 * np.sqrt(6006.0 / np.pi) * (6 * x**5 * y - 20 * x**3 * y**3 + 6 * x * y**5) / r**6 - elif (l, m) == (6, -5): + if (l, m) == (6, -5): return 3.0 / 32.0 * np.sqrt(2002.0 / np.pi) * z * (5 * x**4 * y - 10 * x**2 * y**3 + y**5) / r**6 - elif (l, m) == (6, -4): + if (l, m) == (6, -4): return 3.0 / 32.0 * np.sqrt(91.0 / np.pi) * 4 * x * y * (11 * z**2 - r**2) * (x**2 - y**2) / r**6 - elif (l, m) == (6, -3): + if (l, m) == (6, -3): return 1.0 / 32.0 * np.sqrt(2730.0 / np.pi) * -1 * (11 * z**3 - 3 * z * r**2) * (y**3 - 3 * x**2 * y) / r**6 - elif (l, m) == (6, -2): + if (l, m) == (6, -2): return 1.0 / 64.0 * np.sqrt(2730.0 / np.pi) * 2 * x * y * (33 * z**4 - 18 * z**2 * r**2 + r**4) / r**6 - elif (l, m) == (6, -1): + if (l, m) == (6, -1): return 1.0 / 16.0 * np.sqrt(273.0 / np.pi) * y * (33 * z**5 - 30 * z**3 * r**2 + 5 * z * r**4) / r**6 - elif (l, m) == (6, 0): + if (l, m) == (6, 0): return 1.0 / 32.0 * np.sqrt(13.0 / np.pi) * (231 * z**6 - 315 * z**4 * r**2 + 105 * z**2 * r**4 - 5 * r**6) / r**6 - elif (l, m) == (6, 1): + if (l, m) == (6, 1): return 1.0 / 16.0 * np.sqrt(273.0 / np.pi) * x * (33 * z**5 - 30 * z**3 * r**2 + 5 * z * r**4) / r**6 - elif (l, m) == (6, 2): + if (l, m) == (6, 2): return 1.0 / 64.0 * np.sqrt(2730.0 / np.pi) * (x**2 - y**2) * (33 * z**4 - 18 * z**2 * r**2 + r**4) / r**6 - elif (l, m) == (6, 3): + if (l, m) == (6, 3): return 1.0 / 32.0 * np.sqrt(2730.0 / np.pi) * (11 * z**3 - 3 * z * r**2) * (x**3 - 3 * x * y**2) / r**6 - elif (l, m) == (6, 4): + if (l, m) == (6, 4): return ( 3.0 / 32.0 @@ -198,12 +198,11 @@ def Y_l_m_ref(l=0, m=0, r_cart_rel=None): * (x**2 * (x**2 - 3 * y**2) + y**2 * (y**2 - 3 * x**2)) / r**6 ) - elif (l, m) == (6, 5): + if (l, m) == (6, 5): return 3.0 / 32.0 * np.sqrt(2002.0 / np.pi) * z * (x**5 - 10 * x**3 * y**2 + 5 * x * y**4) / r**6 - elif (l, m) == (6, 6): + if (l, m) == (6, 6): return 1.0 / 64.0 * np.sqrt(6006.0 / np.pi) * (x**6 - 15 * x**4 * y**2 + 15 * x**2 * y**4 - y**6) / r**6 - else: - raise NotImplementedError + raise NotImplementedError num_samples = 1 R_cart = [0.0, 0.0, 1.0] diff --git a/tests/test_MOs.py b/tests/test_MOs.py index 60453ac3..daa867d5 100755 --- a/tests/test_MOs.py +++ b/tests/test_MOs.py @@ -457,8 +457,11 @@ def test_MOs_comparing_analytic_and_auto_grads(): grad_x_auto, grad_y_auto, grad_z_auto = _compute_MOs_grad_autodiff(mos_data=mos_data, r_carts=r_carts) - # Path crosses ao_grad_lap (fp64) -> mo_grad (fp64); use min. - atol, rtol = get_tolerance_min(["ao_grad_lap", "mo_grad"], "strict") + # Autodiff differentiates compute_MOs -> compute_AOs (ao_eval, fp32 in + # mixed mode); analytic path uses compute_AOs_grad (ao_grad_lap, fp64). + # Achievable agreement is bounded by ao_eval (the fp32 forward kernel the + # autodiff side runs through), not by ao_grad_lap or mo_grad. + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap", "mo_grad"], "strict") assert not np.any(np.isnan(np.asarray(grad_x_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_x_auto))), "NaN detected in second argument" np.testing.assert_allclose(grad_x_an, grad_x_auto, atol=atol, rtol=rtol) @@ -525,8 +528,11 @@ def test_MOs_comparing_analytic_and_auto_laplacians(): mo_lap_auto = _compute_MOs_laplacian_autodiff(mos_data=mos_data, r_carts=r_carts) - # Path crosses ao_grad_lap (fp64) -> mo_lap (fp64); use min. - atol, rtol = get_tolerance_min(["ao_grad_lap", "mo_lap"], "strict") + # Autodiff differentiates compute_MOs -> compute_AOs (ao_eval, fp32 in + # mixed mode); analytic path uses compute_AOs_laplacian (ao_grad_lap, + # fp64). Achievable agreement is bounded by ao_eval (the fp32 forward + # kernel the autodiff side runs through), not by ao_grad_lap or mo_lap. + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap", "mo_lap"], "strict") assert not np.any(np.isnan(np.asarray(mo_lap_an))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_lap_auto))), "NaN detected in second argument" np.testing.assert_allclose(mo_lap_an, mo_lap_auto, atol=atol, rtol=rtol) @@ -536,7 +542,6 @@ def test_MOs_comparing_analytic_and_auto_laplacians(): def test_MOs_sphe_to_cart(): """Ensure spherical -> Cartesian conversion preserves MO values up to l=6.""" - rng = np.random.default_rng(0) nucleus_index = [] @@ -627,7 +632,6 @@ def test_MOs_sphe_to_cart(): def test_MOs_cart_to_sphe(): """Ensure Cartesian -> spherical conversion preserves MO values up to l=6.""" - rng = np.random.default_rng(1) nucleus_index = [] diff --git a/tests/test_ao_basis_optimization.py b/tests/test_ao_basis_optimization.py index 72d1d102..39fb93d9 100644 --- a/tests/test_ao_basis_optimization.py +++ b/tests/test_ao_basis_optimization.py @@ -14,7 +14,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc.atomic_orbital import AOs_cart_data, AOs_sphe_data, ShellPrimMap +from jqmc.atomic_orbital import ShellPrimMap from jqmc.determinant import Geminal_data, compute_det_geminal_all_elements from jqmc.jastrow_factor import ( Jastrow_data, @@ -22,7 +22,6 @@ compute_Jastrow_three_body, ) from jqmc._precision import get_tolerance -from jqmc.molecular_orbital import MOs_data from jqmc.trexio_wrapper import read_trexio_file from jqmc.wavefunction import ( VariationalParameterBlock, @@ -724,7 +723,6 @@ def test_shell_symmetrize_selection_mask(): def test_opt_with_projected_MOs_lambda_basis_conflict(): """opt_with_projected_MOs should raise ValueError when combined with lambda basis optimization.""" - from jqmc.jqmc_mcmc import MCMC # Only opt_lambda_basis_exp/coeff conflict with opt_with_projected_MOs. # opt_J3_basis_exp/coeff are allowed because J3 basis does not affect diff --git a/tests/test_checkpoint_components.py b/tests/test_checkpoint_components.py index cd020053..be270fde 100644 --- a/tests/test_checkpoint_components.py +++ b/tests/test_checkpoint_components.py @@ -20,7 +20,6 @@ load_observables_from_checkpoint, load_optax_state, load_rank_checkpoint, - merge_rank_checkpoints, save_optax_state, save_rank_checkpoint, ) @@ -167,7 +166,6 @@ def test_save_load_roundtrip(self, tmp_path): assert os.path.exists(rank_file) # Now merge into a single checkpoint (simulate rank 0) - from jqmc._checkpoint import merge_rank_checkpoints # We need a Hamiltonian_data for merging -- use a minimal mock merged = str(tmp_path / "restart.h5") diff --git a/tests/test_determinant.py b/tests/test_determinant.py index 549898e0..8738daf1 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -1732,7 +1732,8 @@ def _build_geminal_inverse(geminal_data, r_up_carts, r_dn_carts): def test_streaming_det_state_against_full(trexio_file: str): """Det streaming state, after K random single-electron moves, must reproduce ``compute_grads_and_laplacian_ln_Det_fast`` at the resulting - configuration.""" + configuration. + """ ( _, _, diff --git a/tests/test_init_electron_configurations.py b/tests/test_init_electron_configurations.py index 238c0b18..41561c1a 100644 --- a/tests/test_init_electron_configurations.py +++ b/tests/test_init_electron_configurations.py @@ -303,7 +303,8 @@ def test_dimer_per_atom_counts_match_reference_for_all_S(label, zeta_pair): @pytest.mark.parametrize("elem_z", [3, 7]) def test_linear_triatomic_charge_neutrality(elem_z): """For a homonuclear linear triatomic at separation 5 bohr each, every atom - receives exactly zeta electrons (charge-neutral assignment).""" + receives exactly zeta electrons (charge-neutral assignment). + """ charges = np.array([float(elem_z)] * 3) coords = np.array([[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [10.0, 0.0, 0.0]]) nion = 3 diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index 072c1edb..93feab66 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -1752,7 +1752,8 @@ def test_streaming_J2_state_against_full(j2b_type, n_up, n_dn): def test_streaming_J3_state_against_full(trexio_file): """K random single-electron moves advanced via the J3 streaming state must match a fresh init at the resulting configuration (and the existing analytic - full computation) within strict tolerance.""" + full computation) within strict tolerance. + """ import os from jqmc.jastrow_factor import ( diff --git a/tests/test_jqmc_gfmc_bra.py b/tests/test_jqmc_gfmc_bra.py index f32f03d8..b6710711 100755 --- a/tests/test_jqmc_gfmc_bra.py +++ b/tests/test_jqmc_gfmc_bra.py @@ -45,7 +45,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc._precision import get_tolerance_min from jqmc.hamiltonians import Hamiltonian_data from jqmc.jastrow_factor import ( Jastrow_data, diff --git a/tests/test_jqmc_gfmc_tau.py b/tests/test_jqmc_gfmc_tau.py index bcc788ab..77a7adc1 100755 --- a/tests/test_jqmc_gfmc_tau.py +++ b/tests/test_jqmc_gfmc_tau.py @@ -45,7 +45,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc._precision import get_tolerance_min from jqmc.hamiltonians import Hamiltonian_data from jqmc.jastrow_factor import ( Jastrow_data, diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index f539c344..5f8149ab 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -47,7 +47,7 @@ if project_root not in sys.path: sys.path.insert(0, project_root) -from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc._precision import get_tolerance_min from jqmc.determinant import Geminal_data from jqmc.hamiltonians import Hamiltonian_data from jqmc.jastrow_factor import ( @@ -414,7 +414,7 @@ def fake_apply_block_updates(self, blocks, thetas, learning_rate): def fake_run(self, num_mcmc_steps: int = 0, max_time=None): """No-op MCMC run to skip sampling in the unit test.""" - return None + return def fake_get_dln_WF( self, diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index 11b166be..b6e2a2db 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -725,7 +725,8 @@ def _build_A_inv_from_carts(geminal_data, r_up_jnp, r_dn_jnp): def _streaming_step_consistency_one(wavefunction_data, r_up0, r_dn0, K, atol, rtol, seed=0): """Run K random single-electron moves through the streaming state and compare the resulting kinetic energies with a fresh fast-update call at - the final configuration.""" + the final configuration. + """ rng = np.random.RandomState(seed) r_up = np.asarray(r_up0, dtype=np.float64).copy() r_dn = np.asarray(r_dn0, dtype=np.float64).copy() @@ -839,7 +840,8 @@ def _build_wavefunction_J3(trexio_file, j2_type="exp", with_J1=False, with_J2=Tr def test_streaming_kinetic_energy_step_consistency(trexio_file): """K=32 random single-electron moves advanced via the streaming kinetic state must reproduce the fresh fast-update kinetic energy at the resulting - configuration within strict tolerance.""" + configuration within strict tolerance. + """ wf, gem = _build_wavefunction_J3(trexio_file) n_up = gem.num_electron_up n_dn = gem.num_electron_dn @@ -854,7 +856,8 @@ def test_streaming_kinetic_energy_step_consistency(trexio_file): def test_streaming_kinetic_drift_accumulation(K): """Drift accumulation: K-step advance vs fresh init at config_K must stay within ``loose`` tolerance even at K=1000, which sets the safety margin - for ``num_mcmc_per_measurement``.""" + for ``num_mcmc_per_measurement``. + """ wf, gem = _build_wavefunction_J3("H2_ae_ccpvdz_cart.h5") rng = np.random.RandomState(1) r_up0 = 4.0 * rng.rand(gem.num_electron_up, 3) - 2.0 @@ -869,7 +872,8 @@ def test_streaming_kinetic_drift_accumulation(K): ) def test_streaming_kinetic_edge_cases(trexio_file): """Edge cases: small electron counts and ``N_up != N_dn`` (Li, N) must - still match the fresh fast-update result.""" + still match the fresh fast-update result. + """ wf, gem = _build_wavefunction_J3(trexio_file) rng = np.random.RandomState(3) r_up0 = 4.0 * rng.rand(gem.num_electron_up, 3) - 2.0 @@ -881,7 +885,8 @@ def test_streaming_kinetic_edge_cases(trexio_file): @pytest.mark.parametrize("jastrow_combo", ["J3_only", "J1_J3", "J2_J3", "J1_J2_J3"]) def test_streaming_kinetic_jastrow_combinations(jastrow_combo): """Streaming path must work for every J3-containing Jastrow combination - (PR1 dispatch requires J3 + ``jastrow_nn_data is None``).""" + (PR1 dispatch requires J3 + ``jastrow_nn_data is None``). + """ with_J1 = "J1" in jastrow_combo with_J2 = "J2" in jastrow_combo wf, gem = _build_wavefunction_J3("water_ccecp_ccpvqz.h5", with_J1=with_J1, with_J2=with_J2) @@ -895,7 +900,8 @@ def test_streaming_kinetic_jastrow_combinations(jastrow_combo): def test_streaming_kinetic_walker_axis_vmap(): """``vmap`` over the walker axis must produce results equal to the independent per-walker streaming chains. Confirms the state pytree carries - walkers correctly along the leading axis.""" + walkers correctly along the leading axis. + """ wf, gem = _build_wavefunction_J3("H2_ae_ccpvdz_cart.h5") n_walkers = 4 rng = np.random.RandomState(7) diff --git a/tools/_read_from_turbo_wf_and_ecp.py b/tools/_read_from_turbo_wf_and_ecp.py index 2f53b7fa..4bda3ec8 100644 --- a/tools/_read_from_turbo_wf_and_ecp.py +++ b/tools/_read_from_turbo_wf_and_ecp.py @@ -6,10 +6,8 @@ from jqmc.atomic_orbital import AOs_data from jqmc.coulomb_potential import Coulomb_potential_data from jqmc.determinant import Geminal_data -from jqmc.jastrow_factor import Jastrow_data, Jastrow_three_body_data, Jastrow_two_body_data from jqmc.molecular_orbital import MOs_data, compute_MOs_api from jqmc.structure import Structure_data -from jqmc.vmc import VMC # structure io_fort10 = IO_fort10() diff --git a/tools/read_Jastrow_factor_from_turbo_wf.py b/tools/read_Jastrow_factor_from_turbo_wf.py index 320e64db..98cac68a 100644 --- a/tools/read_Jastrow_factor_from_turbo_wf.py +++ b/tools/read_Jastrow_factor_from_turbo_wf.py @@ -1,6 +1,5 @@ # read turborvb WF and PPs and generate jQMC WF and PP instances. import sys -import pickle import json import numpy as np From 90c52378daad1a4d40e93e8d8d7a8a109ddb826c Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 14:16:01 +0900 Subject: [PATCH 50/97] Polish the tolerance control in pytest. --- jqmc/_setting.py | 22 +- jqmc/atomic_orbital.py | 48 ++-- jqmc/jastrow_factor.py | 440 ++++++++++-------------------------- jqmc/molecular_orbital.py | 48 ++-- tests/test_AOs.py | 10 +- tests/test_MOs.py | 6 +- tests/test_determinant.py | 2 +- tests/test_jastrow.py | 10 +- tests/test_wave_function.py | 43 +++- 9 files changed, 218 insertions(+), 411 deletions(-) diff --git a/jqmc/_setting.py b/jqmc/_setting.py index 8745e3cf..ad9d6e1d 100644 --- a/jqmc/_setting.py +++ b/jqmc/_setting.py @@ -80,15 +80,23 @@ # zone's current dtype and returns ``(atol, rtol)``. # # Levels: -# strict -- two exact implementations of the same quantity (debug vs -# production, analytic vs autodiff). Difference is pure -# floating-point round-off. -# loose -- comparison involving numerical differentiation or quadrature. -# Finite-difference truncation error dominates, so tolerances -# are much wider. +# strict -- two exact implementations of the same quantity (debug vs +# production, analytic vs autodiff). Difference is pure +# floating-point round-off. +# autodiff -- analytic (uses the dedicated grad/lap zones, e.g. ao_grad_lap +# which is fp64 even in mixed mode) vs autodiff (jax.grad / +# jax.hessian of forward evaluators, which inherit the forward +# zone dtype, e.g. ao_eval = fp32 in mixed mode). fp64 +# tolerance is the same as strict (both paths run at fp64); +# fp32 tolerance is widened to absorb the autodiff-side fp32 +# grad/lap noise that the analytic path does not see. +# loose -- comparison involving numerical differentiation or quadrature. +# Finite-difference truncation error dominates, so tolerances +# are much wider. _TOLERANCE: dict[str, dict[str, tuple[float, float]]] = { "strict": {"float64": (1e-8, 1e-6), "float32": (1e-5, 1e-3)}, - "loose": {"float64": (1e-1, 1e-4), "float32": (1e-1, 1e-3)}, + "autodiff": {"float64": (1e-8, 1e-6), "float32": (1e-4, 1e-2)}, + "loose": {"float64": (1e-3, 5e-4), "float32": (1e-1, 1e-3)}, } # --- Dtype-aware EPS constants --- diff --git a/jqmc/atomic_orbital.py b/jqmc/atomic_orbital.py index d5ffba95..0a046d1c 100755 --- a/jqmc/atomic_orbital.py +++ b/jqmc/atomic_orbital.py @@ -3128,38 +3128,28 @@ def _compute_AOs_laplacian_debug( Array containing laplacians of the AOs at r_carts. The dim. is (num_ao, N_e) """ - # Laplacians of AOs (numerical) - diff_h = 1.0e-5 + # Laplacians of AOs (numerical, 4th-order central FD) + # f''(x) ~= (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h^2) + # Larger h is viable with the 4th-order stencil (O(h^4) truncation). + diff_h = 1.0e-3 ao_matrix = compute_AOs(aos_data, r_carts) - # laplacians x^2 - diff_p_x_r_carts = r_carts.copy() - diff_p_x_r_carts[:, 0] += diff_h - ao_matrix_diff_p_x = compute_AOs(aos_data, diff_p_x_r_carts) - diff_m_x_r_carts = r_carts.copy() - diff_m_x_r_carts[:, 0] -= diff_h - ao_matrix_diff_m_x = compute_AOs(aos_data, diff_m_x_r_carts) - - # laplacians y^2 - diff_p_y_r_carts = r_carts.copy() - diff_p_y_r_carts[:, 1] += diff_h - ao_matrix_diff_p_y = compute_AOs(aos_data, diff_p_y_r_carts) - diff_m_y_r_carts = r_carts.copy() - diff_m_y_r_carts[:, 1] -= diff_h - ao_matrix_diff_m_y = compute_AOs(aos_data, diff_m_y_r_carts) - - # laplacians z^2 - diff_p_z_r_carts = r_carts.copy() - diff_p_z_r_carts[:, 2] += diff_h - ao_matrix_diff_p_z = compute_AOs(aos_data, diff_p_z_r_carts) - diff_m_z_r_carts = r_carts.copy() - diff_m_z_r_carts[:, 2] -= diff_h - ao_matrix_diff_m_z = compute_AOs(aos_data, diff_m_z_r_carts) - - ao_matrix_grad2_x = (ao_matrix_diff_p_x + ao_matrix_diff_m_x - 2 * ao_matrix) / (diff_h) ** 2 - ao_matrix_grad2_y = (ao_matrix_diff_p_y + ao_matrix_diff_m_y - 2 * ao_matrix) / (diff_h) ** 2 - ao_matrix_grad2_z = (ao_matrix_diff_p_z + ao_matrix_diff_m_z - 2 * ao_matrix) / (diff_h) ** 2 + def _shifted(dim: int, mult: int): + rs = r_carts.copy() + rs[:, dim] += mult * diff_h + return compute_AOs(aos_data, rs) + + def _grad2_along(dim: int): + f_p1 = _shifted(dim, +1) + f_p2 = _shifted(dim, +2) + f_m1 = _shifted(dim, -1) + f_m2 = _shifted(dim, -2) + return (-f_p2 + 16 * f_p1 - 30 * ao_matrix + 16 * f_m1 - f_m2) / (12 * diff_h**2) + + ao_matrix_grad2_x = _grad2_along(0) + ao_matrix_grad2_y = _grad2_along(1) + ao_matrix_grad2_z = _grad2_along(2) ao_matrix_laplacian = ao_matrix_grad2_x + ao_matrix_grad2_y + ao_matrix_grad2_z diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 0a9e2589..f42c9db6 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -859,73 +859,43 @@ def _compute_grads_and_laplacian_Jastrow_one_body_debug( grad_J1_up = np.array([grad_x_up, grad_y_up, grad_z_up], dtype=dtype_np).T grad_J1_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn], dtype=dtype_np).T - # laplacian + # laplacian (4th-order central FD) + # f''(x) ~= (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h^2) diff_h2 = 1.0e-3 J_ref = _compute_Jastrow_one_body_debug(jastrow_one_body_data, r_up_carts, r_dn_carts) - lap_J1_up = np.zeros(len(r_up_carts), dtype=dtype_np) + def _eval_up(r_up): + return _compute_Jastrow_one_body_debug(jastrow_one_body_data, r_up, r_dn_carts) + + def _eval_dn(r_dn): + return _compute_Jastrow_one_body_debug(jastrow_one_body_data, r_up_carts, r_dn) - # laplacians up + def _fd4_second_deriv(eval_fn, r_carts, r_i, dim, h, f0): + r_p1 = r_carts.copy() + r_p2 = r_carts.copy() + r_m1 = r_carts.copy() + r_m2 = r_carts.copy() + r_p1[r_i][dim] += h + r_p2[r_i][dim] += 2 * h + r_m1[r_i][dim] -= h + r_m2[r_i][dim] -= 2 * h + return (-eval_fn(r_p2) + 16 * eval_fn(r_p1) - 30 * f0 + 16 * eval_fn(r_m1) - eval_fn(r_m2)) / (12 * h**2) + + lap_J1_up = np.zeros(len(r_up_carts), dtype=dtype_np) for r_i, _ in enumerate(r_up_carts): - diff_p_x_r_up2_carts = r_up_carts.copy() - diff_p_y_r_up2_carts = r_up_carts.copy() - diff_p_z_r_up2_carts = r_up_carts.copy() - diff_p_x_r_up2_carts[r_i][0] += diff_h2 - diff_p_y_r_up2_carts[r_i][1] += diff_h2 - diff_p_z_r_up2_carts[r_i][2] += diff_h2 - - J_p_x_up2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, diff_p_x_r_up2_carts, r_dn_carts) - J_p_y_up2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, diff_p_y_r_up2_carts, r_dn_carts) - J_p_z_up2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, diff_p_z_r_up2_carts, r_dn_carts) - - diff_m_x_r_up2_carts = r_up_carts.copy() - diff_m_y_r_up2_carts = r_up_carts.copy() - diff_m_z_r_up2_carts = r_up_carts.copy() - diff_m_x_r_up2_carts[r_i][0] -= diff_h2 - diff_m_y_r_up2_carts[r_i][1] -= diff_h2 - diff_m_z_r_up2_carts[r_i][2] -= diff_h2 - - J_m_x_up2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, diff_m_x_r_up2_carts, r_dn_carts) - J_m_y_up2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, diff_m_y_r_up2_carts, r_dn_carts) - J_m_z_up2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, diff_m_z_r_up2_carts, r_dn_carts) - - gradgrad_x_up = (J_p_x_up2 + J_m_x_up2 - 2 * J_ref) / (diff_h2**2) - gradgrad_y_up = (J_p_y_up2 + J_m_y_up2 - 2 * J_ref) / (diff_h2**2) - gradgrad_z_up = (J_p_z_up2 + J_m_z_up2 - 2 * J_ref) / (diff_h2**2) - - lap_J1_up[r_i] = gradgrad_x_up + gradgrad_y_up + gradgrad_z_up + lap_J1_up[r_i] = ( + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 0, diff_h2, J_ref) + + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 1, diff_h2, J_ref) + + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 2, diff_h2, J_ref) + ) lap_J1_dn = np.zeros(len(r_dn_carts), dtype=dtype_np) - - # laplacians dn for r_i, _ in enumerate(r_dn_carts): - diff_p_x_r_dn2_carts = r_dn_carts.copy() - diff_p_y_r_dn2_carts = r_dn_carts.copy() - diff_p_z_r_dn2_carts = r_dn_carts.copy() - diff_p_x_r_dn2_carts[r_i][0] += diff_h2 - diff_p_y_r_dn2_carts[r_i][1] += diff_h2 - diff_p_z_r_dn2_carts[r_i][2] += diff_h2 - - J_p_x_dn2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, r_up_carts, diff_p_x_r_dn2_carts) - J_p_y_dn2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, r_up_carts, diff_p_y_r_dn2_carts) - J_p_z_dn2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, r_up_carts, diff_p_z_r_dn2_carts) - - diff_m_x_r_dn2_carts = r_dn_carts.copy() - diff_m_y_r_dn2_carts = r_dn_carts.copy() - diff_m_z_r_dn2_carts = r_dn_carts.copy() - diff_m_x_r_dn2_carts[r_i][0] -= diff_h2 - diff_m_y_r_dn2_carts[r_i][1] -= diff_h2 - diff_m_z_r_dn2_carts[r_i][2] -= diff_h2 - - J_m_x_dn2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, r_up_carts, diff_m_x_r_dn2_carts) - J_m_y_dn2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, r_up_carts, diff_m_y_r_dn2_carts) - J_m_z_dn2 = _compute_Jastrow_one_body_debug(jastrow_one_body_data, r_up_carts, diff_m_z_r_dn2_carts) - - gradgrad_x_dn = (J_p_x_dn2 + J_m_x_dn2 - 2 * J_ref) / (diff_h2**2) - gradgrad_y_dn = (J_p_y_dn2 + J_m_y_dn2 - 2 * J_ref) / (diff_h2**2) - gradgrad_z_dn = (J_p_z_dn2 + J_m_z_dn2 - 2 * J_ref) / (diff_h2**2) - - lap_J1_dn[r_i] = gradgrad_x_dn + gradgrad_y_dn + gradgrad_z_dn + lap_J1_dn[r_i] = ( + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 0, diff_h2, J_ref) + + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 1, diff_h2, J_ref) + + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 2, diff_h2, J_ref) + ) return grad_J1_up, grad_J1_dn, lap_J1_up, lap_J1_dn @@ -3306,73 +3276,43 @@ def _compute_grads_and_laplacian_Jastrow_part_debug( grad_J_up = np.array([grad_x_up, grad_y_up, grad_z_up], dtype=dtype_np).T grad_J_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn], dtype=dtype_np).T - # laplacian + # laplacian (4th-order central FD) + # f''(x) ~= (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h^2) diff_h2 = 1.0e-3 J_ref = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - lap_J_up = np.zeros(len(r_up_carts), dtype=dtype_np) + def _eval_up(r_up): + return compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=r_up, r_dn_carts=r_dn_carts) + + def _eval_dn(r_dn): + return compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=r_up_carts, r_dn_carts=r_dn) + + def _fd4_second_deriv(eval_fn, r_carts, r_i, dim, h, f0): + r_p1 = r_carts.copy() + r_p2 = r_carts.copy() + r_m1 = r_carts.copy() + r_m2 = r_carts.copy() + r_p1[r_i][dim] += h + r_p2[r_i][dim] += 2 * h + r_m1[r_i][dim] -= h + r_m2[r_i][dim] -= 2 * h + return (-eval_fn(r_p2) + 16 * eval_fn(r_p1) - 30 * f0 + 16 * eval_fn(r_m1) - eval_fn(r_m2)) / (12 * h**2) - # laplacians up + lap_J_up = np.zeros(len(r_up_carts), dtype=dtype_np) for r_i, _ in enumerate(r_up_carts): - diff_p_x_r_up2_carts = r_up_carts.copy() - diff_p_y_r_up2_carts = r_up_carts.copy() - diff_p_z_r_up2_carts = r_up_carts.copy() - diff_p_x_r_up2_carts[r_i][0] += diff_h2 - diff_p_y_r_up2_carts[r_i][1] += diff_h2 - diff_p_z_r_up2_carts[r_i][2] += diff_h2 - - J_p_x_up2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=diff_p_x_r_up2_carts, r_dn_carts=r_dn_carts) - J_p_y_up2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=diff_p_y_r_up2_carts, r_dn_carts=r_dn_carts) - J_p_z_up2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=diff_p_z_r_up2_carts, r_dn_carts=r_dn_carts) - - diff_m_x_r_up2_carts = r_up_carts.copy() - diff_m_y_r_up2_carts = r_up_carts.copy() - diff_m_z_r_up2_carts = r_up_carts.copy() - diff_m_x_r_up2_carts[r_i][0] -= diff_h2 - diff_m_y_r_up2_carts[r_i][1] -= diff_h2 - diff_m_z_r_up2_carts[r_i][2] -= diff_h2 - - J_m_x_up2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=diff_m_x_r_up2_carts, r_dn_carts=r_dn_carts) - J_m_y_up2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=diff_m_y_r_up2_carts, r_dn_carts=r_dn_carts) - J_m_z_up2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=diff_m_z_r_up2_carts, r_dn_carts=r_dn_carts) - - gradgrad_x_up = (J_p_x_up2 + J_m_x_up2 - 2 * J_ref) / (diff_h2**2) - gradgrad_y_up = (J_p_y_up2 + J_m_y_up2 - 2 * J_ref) / (diff_h2**2) - gradgrad_z_up = (J_p_z_up2 + J_m_z_up2 - 2 * J_ref) / (diff_h2**2) - - lap_J_up[r_i] = gradgrad_x_up + gradgrad_y_up + gradgrad_z_up + lap_J_up[r_i] = ( + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 0, diff_h2, J_ref) + + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 1, diff_h2, J_ref) + + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 2, diff_h2, J_ref) + ) lap_J_dn = np.zeros(len(r_dn_carts), dtype=dtype_np) - - # laplacians dn for r_i, _ in enumerate(r_dn_carts): - diff_p_x_r_dn2_carts = r_dn_carts.copy() - diff_p_y_r_dn2_carts = r_dn_carts.copy() - diff_p_z_r_dn2_carts = r_dn_carts.copy() - diff_p_x_r_dn2_carts[r_i][0] += diff_h2 - diff_p_y_r_dn2_carts[r_i][1] += diff_h2 - diff_p_z_r_dn2_carts[r_i][2] += diff_h2 - - J_p_x_dn2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=r_up_carts, r_dn_carts=diff_p_x_r_dn2_carts) - J_p_y_dn2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=r_up_carts, r_dn_carts=diff_p_y_r_dn2_carts) - J_p_z_dn2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=r_up_carts, r_dn_carts=diff_p_z_r_dn2_carts) - - diff_m_x_r_dn2_carts = r_dn_carts.copy() - diff_m_y_r_dn2_carts = r_dn_carts.copy() - diff_m_z_r_dn2_carts = r_dn_carts.copy() - diff_m_x_r_dn2_carts[r_i][0] -= diff_h2 - diff_m_y_r_dn2_carts[r_i][1] -= diff_h2 - diff_m_z_r_dn2_carts[r_i][2] -= diff_h2 - - J_m_x_dn2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=r_up_carts, r_dn_carts=diff_m_x_r_dn2_carts) - J_m_y_dn2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=r_up_carts, r_dn_carts=diff_m_y_r_dn2_carts) - J_m_z_dn2 = compute_Jastrow_part(jastrow_data=jastrow_data, r_up_carts=r_up_carts, r_dn_carts=diff_m_z_r_dn2_carts) - - gradgrad_x_dn = (J_p_x_dn2 + J_m_x_dn2 - 2 * J_ref) / (diff_h2**2) - gradgrad_y_dn = (J_p_y_dn2 + J_m_y_dn2 - 2 * J_ref) / (diff_h2**2) - gradgrad_z_dn = (J_p_z_dn2 + J_m_z_dn2 - 2 * J_ref) / (diff_h2**2) - - lap_J_dn[r_i] = gradgrad_x_dn + gradgrad_y_dn + gradgrad_z_dn + lap_J_dn[r_i] = ( + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 0, diff_h2, J_ref) + + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 1, diff_h2, J_ref) + + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 2, diff_h2, J_ref) + ) return grad_J_up, grad_J_dn, lap_J_up, lap_J_dn @@ -3889,8 +3829,9 @@ def _compute_grads_and_laplacian_Jastrow_two_body_debug( grad_J2_up = np.array([grad_x_up, grad_y_up, grad_z_up], dtype=dtype_np).T grad_J2_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn], dtype=dtype_np).T - # laplacian - diff_h2 = 1.0e-3 # for laplacian + # laplacian (4th-order central FD) + # f''(x) ~= (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h^2) + diff_h2 = 1.0e-3 J2_ref = compute_Jastrow_two_body( jastrow_two_body_data=jastrow_two_body_data, @@ -3898,119 +3839,46 @@ def _compute_grads_and_laplacian_Jastrow_two_body_debug( r_dn_carts=r_dn_carts, ) - lap_J2_up = np.zeros(len(r_up_carts), dtype=dtype_np) - - # laplacians up - for r_i, _ in enumerate(r_up_carts): - diff_p_x_r_up2_carts = r_up_carts.copy() - diff_p_y_r_up2_carts = r_up_carts.copy() - diff_p_z_r_up2_carts = r_up_carts.copy() - diff_p_x_r_up2_carts[r_i][0] += diff_h2 - diff_p_y_r_up2_carts[r_i][1] += diff_h2 - diff_p_z_r_up2_carts[r_i][2] += diff_h2 - - J2_p_x_up2 = compute_Jastrow_two_body( + def _eval_up(r_up): + return compute_Jastrow_two_body( jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=diff_p_x_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - J2_p_y_up2 = compute_Jastrow_two_body( - jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=diff_p_y_r_up2_carts, + r_up_carts=r_up, r_dn_carts=r_dn_carts, ) - J2_p_z_up2 = compute_Jastrow_two_body( + def _eval_dn(r_dn): + return compute_Jastrow_two_body( jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=diff_p_z_r_up2_carts, - r_dn_carts=r_dn_carts, + r_up_carts=r_up_carts, + r_dn_carts=r_dn, ) - diff_m_x_r_up2_carts = r_up_carts.copy() - diff_m_y_r_up2_carts = r_up_carts.copy() - diff_m_z_r_up2_carts = r_up_carts.copy() - diff_m_x_r_up2_carts[r_i][0] -= diff_h2 - diff_m_y_r_up2_carts[r_i][1] -= diff_h2 - diff_m_z_r_up2_carts[r_i][2] -= diff_h2 + def _fd4_second_deriv(eval_fn, r_carts, r_i, dim, h, f0): + r_p1 = r_carts.copy() + r_p2 = r_carts.copy() + r_m1 = r_carts.copy() + r_m2 = r_carts.copy() + r_p1[r_i][dim] += h + r_p2[r_i][dim] += 2 * h + r_m1[r_i][dim] -= h + r_m2[r_i][dim] -= 2 * h + return (-eval_fn(r_p2) + 16 * eval_fn(r_p1) - 30 * f0 + 16 * eval_fn(r_m1) - eval_fn(r_m2)) / (12 * h**2) - J2_m_x_up2 = compute_Jastrow_two_body( - jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=diff_m_x_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - J2_m_y_up2 = compute_Jastrow_two_body( - jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=diff_m_y_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - J2_m_z_up2 = compute_Jastrow_two_body( - jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=diff_m_z_r_up2_carts, - r_dn_carts=r_dn_carts, + lap_J2_up = np.zeros(len(r_up_carts), dtype=dtype_np) + for r_i, _ in enumerate(r_up_carts): + lap_J2_up[r_i] = ( + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 0, diff_h2, J2_ref) + + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 1, diff_h2, J2_ref) + + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 2, diff_h2, J2_ref) ) - gradgrad_x_up = (J2_p_x_up2 + J2_m_x_up2 - 2 * J2_ref) / (diff_h2**2) - gradgrad_y_up = (J2_p_y_up2 + J2_m_y_up2 - 2 * J2_ref) / (diff_h2**2) - gradgrad_z_up = (J2_p_z_up2 + J2_m_z_up2 - 2 * J2_ref) / (diff_h2**2) - - lap_J2_up[r_i] = gradgrad_x_up + gradgrad_y_up + gradgrad_z_up - lap_J2_dn = np.zeros(len(r_dn_carts), dtype=dtype_np) - - # laplacians dn for r_i, _ in enumerate(r_dn_carts): - diff_p_x_r_dn2_carts = r_dn_carts.copy() - diff_p_y_r_dn2_carts = r_dn_carts.copy() - diff_p_z_r_dn2_carts = r_dn_carts.copy() - diff_p_x_r_dn2_carts[r_i][0] += diff_h2 - diff_p_y_r_dn2_carts[r_i][1] += diff_h2 - diff_p_z_r_dn2_carts[r_i][2] += diff_h2 - - J2_p_x_dn2 = compute_Jastrow_two_body( - jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_p_x_r_dn2_carts, - ) - J2_p_y_dn2 = compute_Jastrow_two_body( - jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_p_y_r_dn2_carts, - ) - - J2_p_z_dn2 = compute_Jastrow_two_body( - jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_p_z_r_dn2_carts, - ) - - diff_m_x_r_dn2_carts = r_dn_carts.copy() - diff_m_y_r_dn2_carts = r_dn_carts.copy() - diff_m_z_r_dn2_carts = r_dn_carts.copy() - diff_m_x_r_dn2_carts[r_i][0] -= diff_h2 - diff_m_y_r_dn2_carts[r_i][1] -= diff_h2 - diff_m_z_r_dn2_carts[r_i][2] -= diff_h2 - - J2_m_x_dn2 = compute_Jastrow_two_body( - jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_m_x_r_dn2_carts, - ) - J2_m_y_dn2 = compute_Jastrow_two_body( - jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_m_y_r_dn2_carts, + lap_J2_dn[r_i] = ( + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 0, diff_h2, J2_ref) + + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 1, diff_h2, J2_ref) + + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 2, diff_h2, J2_ref) ) - J2_m_z_dn2 = compute_Jastrow_two_body( - jastrow_two_body_data=jastrow_two_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_m_z_r_dn2_carts, - ) - - gradgrad_x_dn = (J2_p_x_dn2 + J2_m_x_dn2 - 2 * J2_ref) / (diff_h2**2) - gradgrad_y_dn = (J2_p_y_dn2 + J2_m_y_dn2 - 2 * J2_ref) / (diff_h2**2) - gradgrad_z_dn = (J2_p_z_dn2 + J2_m_z_dn2 - 2 * J2_ref) / (diff_h2**2) - - lap_J2_dn[r_i] = gradgrad_x_dn + gradgrad_y_dn + gradgrad_z_dn return grad_J2_up, grad_J2_dn, lap_J2_up, lap_J2_dn @@ -4591,8 +4459,9 @@ def _compute_grads_and_laplacian_Jastrow_three_body_debug( grad_J3_up = np.array([grad_x_up, grad_y_up, grad_z_up], dtype=dtype_np).T grad_J3_dn = np.array([grad_x_dn, grad_y_dn, grad_z_dn], dtype=dtype_np).T - # laplacian - diff_h2 = 1.0e-3 # for laplacian + # laplacian (4th-order central FD) + # f''(x) ~= (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h^2) + diff_h2 = 1.0e-3 J3_ref = compute_Jastrow_three_body( jastrow_three_body_data=jastrow_three_body_data, @@ -4600,119 +4469,46 @@ def _compute_grads_and_laplacian_Jastrow_three_body_debug( r_dn_carts=r_dn_carts, ) - lap_J3_up = np.zeros(len(r_up_carts), dtype=dtype_np) - - # laplacians up - for r_i, _ in enumerate(r_up_carts): - diff_p_x_r_up2_carts = r_up_carts.copy() - diff_p_y_r_up2_carts = r_up_carts.copy() - diff_p_z_r_up2_carts = r_up_carts.copy() - diff_p_x_r_up2_carts[r_i][0] += diff_h2 - diff_p_y_r_up2_carts[r_i][1] += diff_h2 - diff_p_z_r_up2_carts[r_i][2] += diff_h2 - - J3_p_x_up2 = compute_Jastrow_three_body( - jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=diff_p_x_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - J3_p_y_up2 = compute_Jastrow_three_body( + def _eval_up(r_up): + return compute_Jastrow_three_body( jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=diff_p_y_r_up2_carts, + r_up_carts=r_up, r_dn_carts=r_dn_carts, ) - J3_p_z_up2 = compute_Jastrow_three_body( + def _eval_dn(r_dn): + return compute_Jastrow_three_body( jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=diff_p_z_r_up2_carts, - r_dn_carts=r_dn_carts, + r_up_carts=r_up_carts, + r_dn_carts=r_dn, ) - diff_m_x_r_up2_carts = r_up_carts.copy() - diff_m_y_r_up2_carts = r_up_carts.copy() - diff_m_z_r_up2_carts = r_up_carts.copy() - diff_m_x_r_up2_carts[r_i][0] -= diff_h2 - diff_m_y_r_up2_carts[r_i][1] -= diff_h2 - diff_m_z_r_up2_carts[r_i][2] -= diff_h2 + def _fd4_second_deriv(eval_fn, r_carts, r_i, dim, h, f0): + r_p1 = r_carts.copy() + r_p2 = r_carts.copy() + r_m1 = r_carts.copy() + r_m2 = r_carts.copy() + r_p1[r_i][dim] += h + r_p2[r_i][dim] += 2 * h + r_m1[r_i][dim] -= h + r_m2[r_i][dim] -= 2 * h + return (-eval_fn(r_p2) + 16 * eval_fn(r_p1) - 30 * f0 + 16 * eval_fn(r_m1) - eval_fn(r_m2)) / (12 * h**2) - J3_m_x_up2 = compute_Jastrow_three_body( - jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=diff_m_x_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - J3_m_y_up2 = compute_Jastrow_three_body( - jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=diff_m_y_r_up2_carts, - r_dn_carts=r_dn_carts, - ) - J3_m_z_up2 = compute_Jastrow_three_body( - jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=diff_m_z_r_up2_carts, - r_dn_carts=r_dn_carts, + lap_J3_up = np.zeros(len(r_up_carts), dtype=dtype_np) + for r_i, _ in enumerate(r_up_carts): + lap_J3_up[r_i] = ( + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 0, diff_h2, J3_ref) + + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 1, diff_h2, J3_ref) + + _fd4_second_deriv(_eval_up, r_up_carts, r_i, 2, diff_h2, J3_ref) ) - gradgrad_x_up = (J3_p_x_up2 + J3_m_x_up2 - 2 * J3_ref) / (diff_h2**2) - gradgrad_y_up = (J3_p_y_up2 + J3_m_y_up2 - 2 * J3_ref) / (diff_h2**2) - gradgrad_z_up = (J3_p_z_up2 + J3_m_z_up2 - 2 * J3_ref) / (diff_h2**2) - - lap_J3_up[r_i] = gradgrad_x_up + gradgrad_y_up + gradgrad_z_up - lap_J3_dn = np.zeros(len(r_dn_carts), dtype=dtype_np) - - # laplacians dn for r_i, _ in enumerate(r_dn_carts): - diff_p_x_r_dn2_carts = r_dn_carts.copy() - diff_p_y_r_dn2_carts = r_dn_carts.copy() - diff_p_z_r_dn2_carts = r_dn_carts.copy() - diff_p_x_r_dn2_carts[r_i][0] += diff_h2 - diff_p_y_r_dn2_carts[r_i][1] += diff_h2 - diff_p_z_r_dn2_carts[r_i][2] += diff_h2 - - J3_p_x_dn2 = compute_Jastrow_three_body( - jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_p_x_r_dn2_carts, + lap_J3_dn[r_i] = ( + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 0, diff_h2, J3_ref) + + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 1, diff_h2, J3_ref) + + _fd4_second_deriv(_eval_dn, r_dn_carts, r_i, 2, diff_h2, J3_ref) ) - J3_p_y_dn2 = compute_Jastrow_three_body( - jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_p_y_r_dn2_carts, - ) - - J3_p_z_dn2 = compute_Jastrow_three_body( - jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_p_z_r_dn2_carts, - ) - - diff_m_x_r_dn2_carts = r_dn_carts.copy() - diff_m_y_r_dn2_carts = r_dn_carts.copy() - diff_m_z_r_dn2_carts = r_dn_carts.copy() - diff_m_x_r_dn2_carts[r_i][0] -= diff_h2 - diff_m_y_r_dn2_carts[r_i][1] -= diff_h2 - diff_m_z_r_dn2_carts[r_i][2] -= diff_h2 - - J3_m_x_dn2 = compute_Jastrow_three_body( - jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_m_x_r_dn2_carts, - ) - J3_m_y_dn2 = compute_Jastrow_three_body( - jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_m_y_r_dn2_carts, - ) - J3_m_z_dn2 = compute_Jastrow_three_body( - jastrow_three_body_data=jastrow_three_body_data, - r_up_carts=r_up_carts, - r_dn_carts=diff_m_z_r_dn2_carts, - ) - - gradgrad_x_dn = (J3_p_x_dn2 + J3_m_x_dn2 - 2 * J3_ref) / (diff_h2**2) - gradgrad_y_dn = (J3_p_y_dn2 + J3_m_y_dn2 - 2 * J3_ref) / (diff_h2**2) - gradgrad_z_dn = (J3_p_z_dn2 + J3_m_z_dn2 - 2 * J3_ref) / (diff_h2**2) - - lap_J3_dn[r_i] = gradgrad_x_dn + gradgrad_y_dn + gradgrad_z_dn return grad_J3_up, grad_J3_dn, lap_J3_up, lap_J3_dn diff --git a/jqmc/molecular_orbital.py b/jqmc/molecular_orbital.py index 4a291b96..b97599dd 100644 --- a/jqmc/molecular_orbital.py +++ b/jqmc/molecular_orbital.py @@ -302,38 +302,28 @@ def compute_MOs_laplacian(mos_data: MOs_data, r_carts: jax.Array) -> jax.Array: def _compute_MOs_laplacian_debug(mos_data: MOs_data, r_carts: npt.NDArray[np.float64]): """See _api method.""" - # Laplacians of AOs (numerical) - diff_h = 1.0e-5 + # Laplacians of MOs (numerical, 4th-order central FD) + # f''(x) ~= (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h^2) + # Larger h is viable with the 4th-order stencil (O(h^4) truncation). + diff_h = 1.0e-3 mo_matrix = compute_MOs(mos_data, r_carts) - # laplacians x^2 - diff_p_x_r_carts = r_carts.copy() - diff_p_x_r_carts[:, 0] += diff_h - mo_matrix_diff_p_x = compute_MOs(mos_data, diff_p_x_r_carts) - diff_m_x_r_carts = r_carts.copy() - diff_m_x_r_carts[:, 0] -= diff_h - mo_matrix_diff_m_x = compute_MOs(mos_data, diff_m_x_r_carts) - - # laplacians y^2 - diff_p_y_r_carts = r_carts.copy() - diff_p_y_r_carts[:, 1] += diff_h - mo_matrix_diff_p_y = compute_MOs(mos_data, diff_p_y_r_carts) - diff_m_y_r_carts = r_carts.copy() - diff_m_y_r_carts[:, 1] -= diff_h - mo_matrix_diff_m_y = compute_MOs(mos_data, diff_m_y_r_carts) - - # laplacians z^2 - diff_p_z_r_carts = r_carts.copy() - diff_p_z_r_carts[:, 2] += diff_h - mo_matrix_diff_p_z = compute_MOs(mos_data, diff_p_z_r_carts) - diff_m_z_r_carts = r_carts.copy() - diff_m_z_r_carts[:, 2] -= diff_h - mo_matrix_diff_m_z = compute_MOs(mos_data, diff_m_z_r_carts) - - mo_matrix_grad2_x = (mo_matrix_diff_p_x + mo_matrix_diff_m_x - 2 * mo_matrix) / (diff_h) ** 2 - mo_matrix_grad2_y = (mo_matrix_diff_p_y + mo_matrix_diff_m_y - 2 * mo_matrix) / (diff_h) ** 2 - mo_matrix_grad2_z = (mo_matrix_diff_p_z + mo_matrix_diff_m_z - 2 * mo_matrix) / (diff_h) ** 2 + def _shifted(dim: int, mult: int): + rs = r_carts.copy() + rs[:, dim] += mult * diff_h + return compute_MOs(mos_data, rs) + + def _grad2_along(dim: int): + f_p1 = _shifted(dim, +1) + f_p2 = _shifted(dim, +2) + f_m1 = _shifted(dim, -1) + f_m2 = _shifted(dim, -2) + return (-f_p2 + 16 * f_p1 - 30 * mo_matrix + 16 * f_m1 - f_m2) / (12 * diff_h**2) + + mo_matrix_grad2_x = _grad2_along(0) + mo_matrix_grad2_y = _grad2_along(1) + mo_matrix_grad2_z = _grad2_along(2) mo_matrix_laplacian = mo_matrix_grad2_x + mo_matrix_grad2_y + mo_matrix_grad2_z diff --git a/tests/test_AOs.py b/tests/test_AOs.py index d074f6d8..23340861 100755 --- a/tests/test_AOs.py +++ b/tests/test_AOs.py @@ -573,7 +573,7 @@ def test_AOs_sphe_and_cart_grads_auto_vs_numerical(): # autodiff and FD-debug both pass through compute_AOs (ao_eval zone, fp32 in # mixed mode); tolerance bottlenecked by ao_eval. - atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "loose") + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "strict") assert not np.any(np.isnan(np.asarray(gx_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(gx_auto_cart, gx_num_cart, atol=atol, rtol=rtol) @@ -763,7 +763,7 @@ def test_AOs_sphe_and_cart_grads_analytic_vs_numerical(): # FD-debug path goes through compute_AOs (ao_eval zone, fp32 in mixed mode); # tolerance bottlenecked by ao_eval. - atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "loose") + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "strict") assert not np.any(np.isnan(np.asarray(gx_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(gx_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(gx_an_cart, gx_num_cart, atol=atol, rtol=rtol) @@ -977,7 +977,7 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_numerical(): # FD-debug path goes through compute_AOs (ao_eval zone, fp32 in mixed mode); # tolerance bottlenecked by ao_eval. - atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "loose") + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "strict") assert not np.any(np.isnan(np.asarray(lap_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_an_cart, lap_num_cart, atol=atol, rtol=rtol) @@ -1079,7 +1079,7 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): # both autodiff and FD-debug go through compute_AOs (ao_eval zone, fp32 in # mixed mode); tolerance bottlenecked by ao_eval. - atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "loose") + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "strict") assert not np.any(np.isnan(np.asarray(lap_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_auto_cart, lap_num_cart, atol=atol, rtol=rtol) @@ -1372,7 +1372,7 @@ def test_overlap_matrix_sphe_analytic_vs_numerical_debug(): overlap_analytic = np.asarray(compute_overlap_matrix(aos_data=aos_data), dtype=np.float64) overlap_numerical = _compute_overlap_matrix_debug(aos_data=aos_data, num_grid_points=41, tail_tolerance=1.0e-12) - atol_l, rtol_l = get_tolerance("ao_eval", "loose") + atol_l, rtol_l = get_tolerance("ao_eval", "strict") atol_s, rtol_s = get_tolerance("ao_eval", "strict") assert not np.any(np.isnan(np.asarray(overlap_analytic))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(overlap_numerical))), "NaN detected in second argument" diff --git a/tests/test_MOs.py b/tests/test_MOs.py index daa867d5..d21d9480 100755 --- a/tests/test_MOs.py +++ b/tests/test_MOs.py @@ -53,7 +53,6 @@ ) from jqmc.molecular_orbital import ( MOs_data, - _cart_to_spherical_matrix, _compute_MOs_debug, _compute_MOs_grad_autodiff, _compute_MOs_grad_debug, @@ -64,6 +63,7 @@ compute_MOs_laplacian, compute_MOs_value_grad_lap, ) +from jqmc._jqmc_utility import _cart_to_spherical_matrix from jqmc._precision import get_dtype_jnp, get_tolerance, get_tolerance_min from jqmc.structure import Structure_data from jqmc.trexio_wrapper import read_trexio_file @@ -254,7 +254,7 @@ def test_MOs_comparing_auto_and_numerical_grads(): mo_matrix_grad_z_numerical, ) = _compute_MOs_grad_autodiff(mos_data=mos_data, r_carts=r_carts) - atol, rtol = get_tolerance("mo_grad", "loose") + atol, rtol = get_tolerance("mo_grad", "strict") assert not np.any(np.isnan(np.asarray(mo_matrix_grad_x_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_grad_x_numerical))), "NaN detected in second argument" np.testing.assert_allclose(mo_matrix_grad_x_auto, mo_matrix_grad_x_numerical, atol=atol, rtol=rtol) @@ -391,7 +391,7 @@ def test_MOs_comparing_auto_and_numerical_laplacians(): mo_matrix_laplacian_auto = _compute_MOs_laplacian_autodiff(mos_data=mos_data, r_carts=r_carts) - atol, rtol = get_tolerance("mo_lap", "loose") + atol, rtol = get_tolerance("mo_lap", "strict") assert not np.any(np.isnan(np.asarray(mo_matrix_laplacian_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_laplacian_numerical))), "NaN detected in second argument" np.testing.assert_allclose( diff --git a/tests/test_determinant.py b/tests/test_determinant.py index 8738daf1..142105d8 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -1135,7 +1135,7 @@ def _generate_config(): r_dn_carts=r_dn_carts, ) - atol, rtol = get_tolerance("det_grad_lap", "loose") + atol, rtol = get_tolerance("det_grad_lap", "strict") assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_numerical)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index 93feab66..85e3e6da 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -129,7 +129,7 @@ def test_Jastrow_onebody_part(j1b_type): @pytest.mark.parametrize("j1b_type", ["exp", "pade"]) def test_numerical_and_auto_grads_Jastrow_onebody_part(j1b_type): """Test numerical and JAX grads of the one-body Jastrow factor.""" - atol, rtol = get_tolerance("jastrow_grad_lap", "loose") + atol, rtol = get_tolerance("jastrow_grad_lap", "strict") num_r_up_cart_samples = 6 num_r_dn_cart_samples = 3 num_R_cart_samples = 5 @@ -280,7 +280,7 @@ def test_Jastrow_twobody_part(j2b_type): def test_numerical_and_auto_grads_Jastrow_twobody_part(j2b_type): """Test numerical and JAX grads of the two-body Jastrow factor, comparing the debug and JAX implementations.""" atol_s, rtol_s = get_tolerance("jastrow_eval", "strict") - atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "loose") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "strict") num_r_up_cart_samples = 5 num_r_dn_cart_samples = 2 @@ -871,7 +871,7 @@ def test_Jastrow_threebody_part_cart_to_sphe_MOs_data(): def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): """Test numerical and JAX grads of the three-body Jastrow factor, comparing the debug and JAX implementations, using AOs data.""" atol_s, rtol_s = get_tolerance("jastrow_eval", "strict") - atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "loose") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "strict") num_r_up_cart_samples = 4 num_r_dn_cart_samples = 2 num_R_cart_samples = 6 @@ -986,7 +986,7 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): """Test numerical and JAX grads of the three-body Jastrow factor, comparing the debug and JAX implementations, using MOs data.""" atol_s, rtol_s = get_tolerance("jastrow_eval", "strict") - atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "loose") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "strict") num_el = 10 num_mo = 5 num_ao = 3 @@ -1340,7 +1340,7 @@ def _build_jastrow_data_for_part_tests(j1b_type: str = "exp", j2b_type: str = "p @pytest.mark.parametrize("j1b_type,j2b_type,include_nn", _JASTROW_COMBOS) def test_numerical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): """Numerical vs auto-diff gradients/laplacian for J1+J2+J3(+NN).""" - atol, rtol = get_tolerance("jastrow_grad_lap", "loose") + atol, rtol = get_tolerance("jastrow_grad_lap", "strict") jastrow_data, r_up_carts, r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) grad_up_num, grad_dn_num, lap_up_num, lap_dn_num = _compute_grads_and_laplacian_Jastrow_part_debug( diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index b6e2a2db..1b08e5cd 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -134,7 +134,7 @@ def test_kinetic_energy_analytic_and_numerical(trexio_file: str): K_debug = _compute_kinetic_energy_debug(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) K_jax = compute_kinetic_energy(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - atol, rtol = get_tolerance("wf_kinetic", "loose") + atol, rtol = get_tolerance("wf_kinetic", "strict") assert not np.any(np.isnan(np.asarray(np.asarray(K_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(K_jax)))), "NaN detected in second argument" np.testing.assert_allclose( @@ -234,8 +234,18 @@ def test_debug_and_auto_kinetic_energy_all_elements(trexio_file: str): num_ele_up = geminal_mo_data.num_electron_up num_ele_dn = geminal_mo_data.num_electron_dn rng = np.random.default_rng(42) - r_up_carts_np = rng.uniform(-2.0, 2.0, size=(num_ele_up, 3)) - r_dn_carts_np = rng.uniform(-2.0, 2.0, size=(num_ele_dn, 3)) + # Generate electron configuration away from wavefunction nodes to ensure + # numerical 2nd derivatives are well-conditioned (|Psi| >> 0). + from jqmc.wavefunction import evaluate_wavefunction + + for _ in range(200): + r_up_carts_np = rng.uniform(-2.0, 2.0, size=(num_ele_up, 3)) + r_dn_carts_np = rng.uniform(-2.0, 2.0, size=(num_ele_dn, 3)) + psi_val = evaluate_wavefunction(wavefunction_data, r_up_carts_np, r_dn_carts_np) + if abs(psi_val) > 1e-8: + break + else: + pytest.skip("Could not find electron configuration sufficiently far from node") r_up_carts_jnp = jnp.array(r_up_carts_np) r_dn_carts_jnp = jnp.array(r_dn_carts_np) @@ -247,6 +257,8 @@ def test_debug_and_auto_kinetic_energy_all_elements(trexio_file: str): wavefunction_data=wavefunction_data, r_up_carts=r_up_carts_jnp, r_dn_carts=r_dn_carts_jnp ) + # Debug path FDs Psi directly; near AE-cusp accuracy is bounded below the + # strict tolerance (e.g. N AE) even with the 4th-order Laplacian stencil. atol, rtol = get_tolerance("wf_kinetic", "loose") assert not np.any(np.isnan(np.asarray(K_elements_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_up_auto))), "NaN detected in second argument" @@ -318,10 +330,13 @@ def test_auto_and_analytic_kinetic_energy_all_elements(trexio_file: str): ) # T_L crosses ao_eval/jastrow_eval/jastrow_grad_lap/wf_kinetic zones; the - # achievable analytic-vs-auto agreement is bounded by the weakest (fp32 in mixed). + # achievable analytic-vs-auto agreement is bounded by the weakest (fp32 in + # mixed). Autodiff path inherits ao_eval = fp32 in its grad/hessian, so + # use the dedicated 'autodiff' tolerance (= strict for fp64, slightly + # looser for fp32). atol, rtol = get_tolerance_min( ("ao_eval", "jastrow_eval", "jastrow_grad_lap", "wf_kinetic"), - "strict", + "autodiff", ) assert not np.any(np.isnan(np.asarray(K_elements_up_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_up_analytic))), "NaN detected in second argument" @@ -593,8 +608,14 @@ def test_nodal_distance_analytic_vs_debug(trexio_file: str): r_dn_carts=r_dn_carts_jnp, ) - # They should be identical up to numerical noise - atol, rtol = get_tolerance("wf_kinetic", "loose") + # They should be identical up to numerical noise. + # Nodal distance ~ |Psi| / |grad Psi| amplifies any fp32 noise via the + # 1/|grad| factor near the node, so even the dedicated 'autodiff' + # tolerance is too tight in mixed mode. Use 'loose' here. + atol, rtol = get_tolerance_min( + ("ao_eval", "jastrow_eval", "jastrow_grad_lap", "wf_kinetic"), + "loose", + ) np.testing.assert_allclose( np.asarray(nd_analytic), np.asarray(nd_debug), @@ -855,14 +876,16 @@ def test_streaming_kinetic_energy_step_consistency(trexio_file): @pytest.mark.parametrize("K", [32, 100, 1000]) def test_streaming_kinetic_drift_accumulation(K): """Drift accumulation: K-step advance vs fresh init at config_K must stay - within ``loose`` tolerance even at K=1000, which sets the safety margin - for ``num_mcmc_per_measurement``. + within strict tolerance even at K=1000. Empirically the streaming and + fresh paths agree to machine precision because the kinetic-energy + assembly (`wf_kinetic` zone, fp64) is recomputed in both paths from the + same per-step inputs. """ wf, gem = _build_wavefunction_J3("H2_ae_ccpvdz_cart.h5") rng = np.random.RandomState(1) r_up0 = 4.0 * rng.rand(gem.num_electron_up, 3) - 2.0 r_dn0 = 4.0 * rng.rand(gem.num_electron_dn, 3) - 2.0 - atol, rtol = get_tolerance_min(["wf_kinetic", "jastrow_grad_lap"], "loose") + atol, rtol = get_tolerance_min(["wf_kinetic", "jastrow_grad_lap"], "strict") _streaming_step_consistency_one(wf, r_up0, r_dn0, K=K, atol=atol, rtol=rtol, seed=2) From 5aeff6083691098aca7cd9d991af061377b5304f Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 14:58:46 +0900 Subject: [PATCH 51/97] Introduce medium tolerance for the numerical laplacian tests, and abolish autodiff tolerance. --- jqmc/_setting.py | 26 ++++++---- tests/test_AOs.py | 14 ++--- tests/test_MOs.py | 4 +- tests/test_ao_basis_optimization.py | 9 ++-- tests/test_determinant.py | 19 +++---- tests/test_jastrow.py | 79 +++++++++++++++-------------- tests/test_wave_function.py | 21 +++++--- 7 files changed, 95 insertions(+), 77 deletions(-) diff --git a/jqmc/_setting.py b/jqmc/_setting.py index ad9d6e1d..e8f3ae0c 100644 --- a/jqmc/_setting.py +++ b/jqmc/_setting.py @@ -83,19 +83,23 @@ # strict -- two exact implementations of the same quantity (debug vs # production, analytic vs autodiff). Difference is pure # floating-point round-off. -# autodiff -- analytic (uses the dedicated grad/lap zones, e.g. ao_grad_lap -# which is fp64 even in mixed mode) vs autodiff (jax.grad / -# jax.hessian of forward evaluators, which inherit the forward -# zone dtype, e.g. ao_eval = fp32 in mixed mode). fp64 -# tolerance is the same as strict (both paths run at fp64); -# fp32 tolerance is widened to absorb the autodiff-side fp32 -# grad/lap noise that the analytic path does not see. -# loose -- comparison involving numerical differentiation or quadrature. -# Finite-difference truncation error dominates, so tolerances -# are much wider. +# medium -- analytic / autodiff vs 4th-order central finite differences +# of a Laplacian (or higher-derivative-sensitive quantity), +# *or* analytic vs autodiff of a quantity whose autodiff path +# inherits a fp32 forward zone (e.g. ao_eval) and therefore +# carries fp32 grad/lap noise the analytic path does not see. +# 4th-order FD has a round-off floor of ~eps_mach / h^2 with +# h = 1e-3 (~5e-9) and a truncation term ~h^4 * f^(6) that can +# grow O(1e-6) for sharp basis sets / cusp regions. Use for +# numerical_diff tests where the FD side is a 2nd derivative. +# loose -- 2nd-derivative FD applied to Psi itself with subsequent +# 1/Psi division (e.g. T_L via central FD on Psi). Errors +# are bounded by the |Psi|->0 amplification near nodes even +# after node-avoidance sampling; only the kinetic energy +# debug paths in wavefunction.py need this. _TOLERANCE: dict[str, dict[str, tuple[float, float]]] = { "strict": {"float64": (1e-8, 1e-6), "float32": (1e-5, 1e-3)}, - "autodiff": {"float64": (1e-8, 1e-6), "float32": (1e-4, 1e-2)}, + "medium": {"float64": (1e-7, 1e-5), "float32": (1e-4, 1e-2)}, "loose": {"float64": (1e-3, 5e-4), "float32": (1e-1, 1e-3)}, } diff --git a/tests/test_AOs.py b/tests/test_AOs.py index 23340861..c59a43fb 100755 --- a/tests/test_AOs.py +++ b/tests/test_AOs.py @@ -975,9 +975,9 @@ def test_AOs_shpe_and_cart_laplacians_analytic_vs_numerical(): lap_num_cart = _compute_AOs_laplacian_debug(aos_data=aos_data, r_carts=r_carts) lap_an_cart = compute_AOs_laplacian(aos_data=aos_data, r_carts=r_carts) - # FD-debug path goes through compute_AOs (ao_eval zone, fp32 in mixed mode); - # tolerance bottlenecked by ao_eval. - atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "strict") + # 4th-order central FD vs analytic Laplacian; FD has truncation/round-off + # error well above ao_eval round-off. Use medium level. + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "medium") assert not np.any(np.isnan(np.asarray(lap_an_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_an_cart, lap_num_cart, atol=atol, rtol=rtol) @@ -1077,9 +1077,9 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): lap_num_cart = _compute_AOs_laplacian_autodiff(aos_data=aos_data, r_carts=r_carts) lap_auto_cart = _compute_AOs_laplacian_debug(aos_data=aos_data, r_carts=r_carts) - # both autodiff and FD-debug go through compute_AOs (ao_eval zone, fp32 in - # mixed mode); tolerance bottlenecked by ao_eval. - atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "strict") + # 4th-order central FD vs autodiff Laplacian; FD has truncation/round-off + # error well above ao_eval round-off. Use medium level. + atol, rtol = get_tolerance_min(["ao_eval", "ao_grad_lap"], "medium") assert not np.any(np.isnan(np.asarray(lap_auto_cart))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(lap_num_cart))), "NaN detected in second argument" np.testing.assert_allclose(lap_auto_cart, lap_num_cart, atol=atol, rtol=rtol) @@ -1198,7 +1198,7 @@ def test_AOs_shpe_and_cart_laplacians_auto_vs_numerical(): "N_ae_ccpvdz_cart.h5", # Cartesian, larger ], ) -def test_fused_AOs_value_grad_lap_matches_split(trexio_file: str): +def test_AOs_value_grad_lap(trexio_file: str): """Fused ``compute_AOs_value_grad_lap`` matches the standalone APIs. grad parity is bitwise (rtol=atol=0) because the fused kernel mirrors diff --git a/tests/test_MOs.py b/tests/test_MOs.py index d21d9480..6aac6bdd 100755 --- a/tests/test_MOs.py +++ b/tests/test_MOs.py @@ -391,7 +391,7 @@ def test_MOs_comparing_auto_and_numerical_laplacians(): mo_matrix_laplacian_auto = _compute_MOs_laplacian_autodiff(mos_data=mos_data, r_carts=r_carts) - atol, rtol = get_tolerance("mo_lap", "strict") + atol, rtol = get_tolerance("mo_lap", "medium") assert not np.any(np.isnan(np.asarray(mo_matrix_laplacian_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(mo_matrix_laplacian_numerical))), "NaN detected in second argument" np.testing.assert_allclose( @@ -744,7 +744,7 @@ def test_MOs_cart_to_sphe(): "N_ae_ccpvdz_cart.h5", ], ) -def test_fused_MOs_value_grad_lap_matches_split(trexio_file: str): +def test_MOs_value_grad_lap(trexio_file: str): """Fused ``compute_MOs_value_grad_lap`` matches the standalone APIs. All outputs (val/grad/lap) are bounded by ULP-level differences in diff --git a/tests/test_ao_basis_optimization.py b/tests/test_ao_basis_optimization.py index 39fb93d9..0cce03ed 100644 --- a/tests/test_ao_basis_optimization.py +++ b/tests/test_ao_basis_optimization.py @@ -247,7 +247,8 @@ def j3_value(exponents): f_minus = j3_value(jnp.array(exp_minus)) grad_fd[i] = (float(f_plus) - float(f_minus)) / (2 * eps) - npt.assert_allclose(np.array(grad_jax), grad_fd, atol=1e-5, rtol=1e-4) + atol, rtol = get_tolerance("jastrow_eval", "strict") + npt.assert_allclose(np.array(grad_jax), grad_fd, atol=atol, rtol=rtol) # ============================================================ @@ -284,7 +285,8 @@ def j3_value(coefficients): f_minus = j3_value(jnp.array(c_minus)) grad_fd[i] = (float(f_plus) - float(f_minus)) / (2 * eps) - npt.assert_allclose(np.array(grad_jax), grad_fd, atol=1e-5, rtol=1e-4) + atol, rtol = get_tolerance("jastrow_eval", "strict") + npt.assert_allclose(np.array(grad_jax), grad_fd, atol=atol, rtol=rtol) # ============================================================ @@ -319,7 +321,8 @@ def det_value(exponents_up): f_minus = det_value(jnp.array(e_minus)) grad_fd[i] = (float(f_plus) - float(f_minus)) / (2 * eps) - npt.assert_allclose(np.array(grad_jax), grad_fd, atol=1e-4, rtol=1e-3) + atol, rtol = get_tolerance("det_eval", "strict") + npt.assert_allclose(np.array(grad_jax), grad_fd, atol=atol, rtol=rtol) # ============================================================ diff --git a/tests/test_determinant.py b/tests/test_determinant.py index 142105d8..ee5cc6b6 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -1135,38 +1135,39 @@ def _generate_config(): r_dn_carts=r_dn_carts, ) - atol, rtol = get_tolerance("det_grad_lap", "strict") + atol_g, rtol_g = get_tolerance("det_grad_lap", "strict") + atol_l, rtol_l = get_tolerance("det_grad_lap", "medium") assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_numerical)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(grad_ln_D_up_numerical), np.asarray(grad_ln_D_up_auto), - atol=atol, - rtol=rtol, + atol=atol_g, + rtol=rtol_g, ) assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_dn_numerical)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(grad_ln_D_dn_numerical), np.asarray(grad_ln_D_dn_auto), - atol=atol, - rtol=rtol, + atol=atol_g, + rtol=rtol_g, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_up_numerical)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_ln_D_up_numerical), np.asarray(lap_ln_D_up_auto), - rtol=rtol, - atol=atol, + rtol=rtol_l, + atol=atol_l, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_dn_numerical)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_ln_D_dn_numerical), np.asarray(lap_ln_D_dn_auto), - rtol=rtol, - atol=atol, + rtol=rtol_l, + atol=atol_l, ) jax.clear_caches() diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index 85e3e6da..d67d733b 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -127,9 +127,10 @@ def test_Jastrow_onebody_part(j1b_type): @pytest.mark.numerical_diff @pytest.mark.parametrize("j1b_type", ["exp", "pade"]) -def test_numerical_and_auto_grads_Jastrow_onebody_part(j1b_type): - """Test numerical and JAX grads of the one-body Jastrow factor.""" - atol, rtol = get_tolerance("jastrow_grad_lap", "strict") +def test_numerical_and_auto_grads_and_laplacian_Jastrow_onebody_part(j1b_type): + """Test numerical and JAX grads / laplacian of the one-body Jastrow factor.""" + atol_g, rtol_g = get_tolerance("jastrow_grad_lap", "strict") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "medium") num_r_up_cart_samples = 6 num_r_dn_cart_samples = 3 num_R_cart_samples = 5 @@ -166,32 +167,32 @@ def test_numerical_and_auto_grads_Jastrow_onebody_part(j1b_type): assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(grad_up_num), np.asarray(grad_up_auto), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(grad_up_num), np.asarray(grad_up_auto), atol=atol_g, rtol=rtol_g) assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(grad_dn_num), np.asarray(grad_dn_auto), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(grad_dn_num), np.asarray(grad_dn_auto), atol=atol_g, rtol=rtol_g) assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_up_num), np.asarray(lap_up_auto), - rtol=rtol, - atol=atol, + rtol=rtol_l, + atol=atol_l, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_dn_num), np.asarray(lap_dn_auto), - rtol=rtol, - atol=atol, + rtol=rtol_l, + atol=atol_l, ) jax.clear_caches() @pytest.mark.parametrize("j1b_type", ["exp", "pade"]) -def test_analytical_and_auto_grads_Jastrow_onebody_part(j1b_type): +def test_analytical_and_auto_grads_and_laplacian_Jastrow_onebody_part(j1b_type): """Analytic vs auto-diff gradients/laplacian for one-body Jastrow.""" atol, rtol = get_tolerance("jastrow_grad_lap", "strict") num_r_up_cart_samples = 5 @@ -277,10 +278,11 @@ def test_Jastrow_twobody_part(j2b_type): @pytest.mark.activate_if_skip_heavy @pytest.mark.numerical_diff @pytest.mark.parametrize("j2b_type", ["pade", "exp"]) -def test_numerical_and_auto_grads_Jastrow_twobody_part(j2b_type): - """Test numerical and JAX grads of the two-body Jastrow factor, comparing the debug and JAX implementations.""" +def test_numerical_and_auto_grads_and_laplacian_Jastrow_twobody_part(j2b_type): + """Test numerical and JAX grads / laplacian of the two-body Jastrow factor, comparing the debug and JAX implementations.""" atol_s, rtol_s = get_tolerance("jastrow_eval", "strict") - atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "strict") + atol_g, rtol_g = get_tolerance("jastrow_grad_lap", "strict") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "medium") num_r_up_cart_samples = 5 num_r_dn_cart_samples = 2 @@ -331,10 +333,10 @@ def test_numerical_and_auto_grads_Jastrow_twobody_part(j2b_type): assert not np.any(np.isnan(np.asarray(grad_J2_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_J2_up_auto))), "NaN detected in second argument" - np.testing.assert_allclose(grad_J2_up_debug, grad_J2_up_auto, atol=atol_l, rtol=rtol_l) + np.testing.assert_allclose(grad_J2_up_debug, grad_J2_up_auto, atol=atol_g, rtol=rtol_g) assert not np.any(np.isnan(np.asarray(grad_J2_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_J2_dn_auto))), "NaN detected in second argument" - np.testing.assert_allclose(grad_J2_dn_debug, grad_J2_dn_auto, atol=atol_l, rtol=rtol_l) + np.testing.assert_allclose(grad_J2_dn_debug, grad_J2_dn_auto, atol=atol_g, rtol=rtol_g) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J2_up_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J2_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( @@ -357,7 +359,7 @@ def test_numerical_and_auto_grads_Jastrow_twobody_part(j2b_type): @pytest.mark.activate_if_skip_heavy @pytest.mark.parametrize("j2b_type", ["pade", "exp"]) -def test_analytic_and_auto_grads_Jastrow_twobody_part(j2b_type): +def test_analytic_and_auto_grads_and_laplacian_Jastrow_twobody_part(j2b_type): """Analytic vs auto-diff gradients/laplacian for two-body Jastrow.""" atol, rtol = get_tolerance("jastrow_grad_lap", "strict") num_r_up_cart_samples = 5 @@ -868,10 +870,11 @@ def test_Jastrow_threebody_part_cart_to_sphe_MOs_data(): @pytest.mark.activate_if_skip_heavy @pytest.mark.numerical_diff -def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): - """Test numerical and JAX grads of the three-body Jastrow factor, comparing the debug and JAX implementations, using AOs data.""" +def test_numerical_and_auto_grads_and_laplacian_Jastrow_threebody_part_with_AOs_data(): + """Test numerical and JAX grads / laplacian of the three-body Jastrow factor, comparing the debug and JAX implementations, using AOs data.""" atol_s, rtol_s = get_tolerance("jastrow_eval", "strict") - atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "strict") + atol_g, rtol_g = get_tolerance("jastrow_grad_lap", "strict") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "medium") num_r_up_cart_samples = 4 num_r_dn_cart_samples = 2 num_R_cart_samples = 6 @@ -957,10 +960,10 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_up_auto))), "NaN detected in second argument" - np.testing.assert_allclose(grad_jastrow_J3_up_debug, grad_jastrow_J3_up_auto, atol=atol_l, rtol=rtol_l) + np.testing.assert_allclose(grad_jastrow_J3_up_debug, grad_jastrow_J3_up_auto, atol=atol_g, rtol=rtol_g) assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_dn_auto))), "NaN detected in second argument" - np.testing.assert_allclose(grad_jastrow_J3_dn_debug, grad_jastrow_J3_dn_auto, atol=atol_l, rtol=rtol_l) + np.testing.assert_allclose(grad_jastrow_J3_dn_debug, grad_jastrow_J3_dn_auto, atol=atol_g, rtol=rtol_g) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_up_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( @@ -983,10 +986,11 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): @pytest.mark.activate_if_skip_heavy @pytest.mark.numerical_diff -def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): - """Test numerical and JAX grads of the three-body Jastrow factor, comparing the debug and JAX implementations, using MOs data.""" +def test_numerical_and_auto_grads_and_laplacian_Jastrow_threebody_part_with_MOs_data(): + """Test numerical and JAX grads / laplacian of the three-body Jastrow factor, comparing the debug and JAX implementations, using MOs data.""" atol_s, rtol_s = get_tolerance("jastrow_eval", "strict") - atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "strict") + atol_g, rtol_g = get_tolerance("jastrow_grad_lap", "strict") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "medium") num_el = 10 num_mo = 5 num_ao = 3 @@ -1076,10 +1080,10 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_up_jax))), "NaN detected in second argument" - np.testing.assert_allclose(grad_jastrow_J3_up_debug, grad_jastrow_J3_up_jax, atol=atol_l, rtol=rtol_l) + np.testing.assert_allclose(grad_jastrow_J3_up_debug, grad_jastrow_J3_up_jax, atol=atol_g, rtol=rtol_g) assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_dn_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(grad_jastrow_J3_dn_jax))), "NaN detected in second argument" - np.testing.assert_allclose(grad_jastrow_J3_dn_debug, grad_jastrow_J3_dn_jax, atol=atol_l, rtol=rtol_l) + np.testing.assert_allclose(grad_jastrow_J3_dn_debug, grad_jastrow_J3_dn_jax, atol=atol_g, rtol=rtol_g) assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_up_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_J3_up_jax)))), "NaN detected in second argument" @@ -1102,7 +1106,7 @@ def test_numerical_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): @pytest.mark.activate_if_skip_heavy -def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): +def test_analytic_and_auto_grads_and_laplacian_Jastrow_threebody_part_with_AOs_data(): """Analytic vs auto-diff gradients/laplacian for three-body Jastrow (AOs).""" # J3 grad/lap crosses two zones: jastrow_grad_lap (fp32 mixed) + ao_grad_lap (fp64). # Use the looser of the two -- under mixed precision, jastrow_grad_lap dominates. @@ -1176,7 +1180,7 @@ def test_analytic_and_auto_grads_Jastrow_threebody_part_with_AOs_data(): @pytest.mark.activate_if_skip_heavy -def test_analytic_and_auto_grads_Jastrow_threebody_part_with_MOs_data(): +def test_analytic_and_auto_grads_and_laplacian_Jastrow_threebody_part_with_MOs_data(): """Analytic vs auto-diff gradients/laplacian for three-body Jastrow (MOs).""" # J3-with-MOs crosses jastrow_grad_lap (fp32 mixed) + ao_grad_lap + mo_grad + mo_lap. # All non-jastrow zones are fp64; jastrow_grad_lap dominates as the loosest. @@ -1338,9 +1342,10 @@ def _build_jastrow_data_for_part_tests(j1b_type: str = "exp", j2b_type: str = "p @pytest.mark.activate_if_skip_heavy @pytest.mark.numerical_diff @pytest.mark.parametrize("j1b_type,j2b_type,include_nn", _JASTROW_COMBOS) -def test_numerical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): +def test_numerical_and_auto_grads_and_laplacian_Jastrow_part(j1b_type, j2b_type, include_nn): """Numerical vs auto-diff gradients/laplacian for J1+J2+J3(+NN).""" - atol, rtol = get_tolerance("jastrow_grad_lap", "strict") + atol_g, rtol_g = get_tolerance("jastrow_grad_lap", "strict") + atol_l, rtol_l = get_tolerance("jastrow_grad_lap", "medium") jastrow_data, r_up_carts, r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) grad_up_num, grad_dn_num, lap_up_num, lap_dn_num = _compute_grads_and_laplacian_Jastrow_part_debug( @@ -1357,26 +1362,26 @@ def test_numerical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(grad_up_num), np.asarray(grad_up_auto), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(grad_up_num), np.asarray(grad_up_auto), atol=atol_g, rtol=rtol_g) assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(grad_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose(np.asarray(grad_dn_num), np.asarray(grad_dn_auto), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(grad_dn_num), np.asarray(grad_dn_auto), atol=atol_g, rtol=rtol_g) assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_up_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_up_num), np.asarray(lap_up_auto), - rtol=rtol, - atol=atol, + rtol=rtol_l, + atol=atol_l, ) assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_num)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(lap_dn_auto)))), "NaN detected in second argument" np.testing.assert_allclose( np.asarray(lap_dn_num), np.asarray(lap_dn_auto), - rtol=rtol, - atol=atol, + rtol=rtol_l, + atol=atol_l, ) jax.clear_caches() @@ -1384,7 +1389,7 @@ def test_numerical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): @pytest.mark.activate_if_skip_heavy @pytest.mark.parametrize("j1b_type,j2b_type,include_nn", _JASTROW_COMBOS) -def test_analytical_and_auto_grads_Jastrow_part(j1b_type, j2b_type, include_nn): +def test_analytical_and_auto_grads_and_laplacian_Jastrow_part(j1b_type, j2b_type, include_nn): """Analytic vs auto-diff gradients/laplacian for J1+J2+J3(+NN).""" # Combined J1+J2+J3(+NN) grad/lap crosses jastrow_grad_lap (fp32 mixed) and the # AO/MO grad/lap zones via the J3 path. jastrow_grad_lap is the loosest under mixed. diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index 1b08e5cd..0833be1a 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -134,7 +134,7 @@ def test_kinetic_energy_analytic_and_numerical(trexio_file: str): K_debug = _compute_kinetic_energy_debug(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) K_jax = compute_kinetic_energy(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - atol, rtol = get_tolerance("wf_kinetic", "strict") + atol, rtol = get_tolerance("wf_kinetic", "medium") assert not np.any(np.isnan(np.asarray(np.asarray(K_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(K_jax)))), "NaN detected in second argument" np.testing.assert_allclose( @@ -257,8 +257,12 @@ def test_debug_and_auto_kinetic_energy_all_elements(trexio_file: str): wavefunction_data=wavefunction_data, r_up_carts=r_up_carts_jnp, r_dn_carts=r_dn_carts_jnp ) - # Debug path FDs Psi directly; near AE-cusp accuracy is bounded below the - # strict tolerance (e.g. N AE) even with the 4th-order Laplacian stencil. + # Debug path FDs Psi directly (2nd-order central FD with 1/Psi division), + # which is fundamentally rougher than FD on ln|Psi|. Near AE nuclear + # cusps (e.g. N all-electron) the high-order derivative growth combined + # with 1/Psi amplification puts the achievable accuracy below medium, + # so this test is held to loose tolerance as a documented exception + # to the "laplacian -> medium" rule. atol, rtol = get_tolerance("wf_kinetic", "loose") assert not np.any(np.isnan(np.asarray(K_elements_up_debug))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_up_auto))), "NaN detected in second argument" @@ -332,11 +336,12 @@ def test_auto_and_analytic_kinetic_energy_all_elements(trexio_file: str): # T_L crosses ao_eval/jastrow_eval/jastrow_grad_lap/wf_kinetic zones; the # achievable analytic-vs-auto agreement is bounded by the weakest (fp32 in # mixed). Autodiff path inherits ao_eval = fp32 in its grad/hessian, so - # use the dedicated 'autodiff' tolerance (= strict for fp64, slightly - # looser for fp32). + # use the 'medium' tolerance (slightly looser than strict in fp64 to + # absorb the autodiff-side fp32 grad/lap noise the analytic path does + # not see). atol, rtol = get_tolerance_min( ("ao_eval", "jastrow_eval", "jastrow_grad_lap", "wf_kinetic"), - "autodiff", + "medium", ) assert not np.any(np.isnan(np.asarray(K_elements_up_auto))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(K_elements_up_analytic))), "NaN detected in second argument" @@ -610,8 +615,8 @@ def test_nodal_distance_analytic_vs_debug(trexio_file: str): # They should be identical up to numerical noise. # Nodal distance ~ |Psi| / |grad Psi| amplifies any fp32 noise via the - # 1/|grad| factor near the node, so even the dedicated 'autodiff' - # tolerance is too tight in mixed mode. Use 'loose' here. + # 1/|grad| factor near the node, so even the 'medium' tolerance is too + # tight in mixed mode. Use 'loose' here. atol, rtol = get_tolerance_min( ("ao_eval", "jastrow_eval", "jastrow_grad_lap", "wf_kinetic"), "loose", From f05ab94dd76fcff25ad6d0c498373ec0b5bded42 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 17:52:52 +0900 Subject: [PATCH 52/97] Remove loose tolerance. --- jqmc/_precision.py | 4 +- jqmc/_setting.py | 11 ++-- jqmc/wavefunction.py | 84 +++++++++++++++++------------ tests/test_wave_function.py | 103 ++---------------------------------- 4 files changed, 58 insertions(+), 144 deletions(-) diff --git a/jqmc/_precision.py b/jqmc/_precision.py index 78839e3a..64c5627c 100644 --- a/jqmc/_precision.py +++ b/jqmc/_precision.py @@ -518,7 +518,7 @@ def get_tolerance(zone: str, level: str = "strict") -> tuple[float, float]: Args: zone: Precision Zone name. - level: ``"strict"`` or ``"loose"``. + level: ``"strict"`` or ``"medium"``. Returns: ``(atol, rtol)`` tuple appropriate for the zone's current dtype. @@ -537,7 +537,7 @@ def get_tolerance_min(zones, level: str = "strict") -> tuple[float, float]: Args: zones: Iterable of Precision Zone names. - level: ``"strict"`` or ``"loose"``. + level: ``"strict"`` or ``"medium"``. Returns: ``(atol, rtol)`` tuple using the maximum of each component. diff --git a/jqmc/_setting.py b/jqmc/_setting.py index e8f3ae0c..ef6ea535 100644 --- a/jqmc/_setting.py +++ b/jqmc/_setting.py @@ -90,17 +90,12 @@ # carries fp32 grad/lap noise the analytic path does not see. # 4th-order FD has a round-off floor of ~eps_mach / h^2 with # h = 1e-3 (~5e-9) and a truncation term ~h^4 * f^(6) that can -# grow O(1e-6) for sharp basis sets / cusp regions. Use for -# numerical_diff tests where the FD side is a 2nd derivative. -# loose -- 2nd-derivative FD applied to Psi itself with subsequent -# 1/Psi division (e.g. T_L via central FD on Psi). Errors -# are bounded by the |Psi|->0 amplification near nodes even -# after node-avoidance sampling; only the kinetic energy -# debug paths in wavefunction.py need this. +# grow O(1e-6) for sharp basis sets / cusp regions (e.g. +# all-electron Z >= 7). Use for numerical_diff tests where the +# FD side is a 2nd derivative. _TOLERANCE: dict[str, dict[str, tuple[float, float]]] = { "strict": {"float64": (1e-8, 1e-6), "float32": (1e-5, 1e-3)}, "medium": {"float64": (1e-7, 1e-5), "float32": (1e-4, 1e-2)}, - "loose": {"float64": (1e-3, 5e-4), "float32": (1e-1, 1e-3)}, } # --- Dtype-aware EPS constants --- diff --git a/jqmc/wavefunction.py b/jqmc/wavefunction.py index 58661bf1..a49da8dd 100644 --- a/jqmc/wavefunction.py +++ b/jqmc/wavefunction.py @@ -986,22 +986,34 @@ def _compute_kinetic_energy_all_elements_debug( ) -> float | complex: """See compute_kinetic_energy_api. - Uses 4th-order central finite differences for the Laplacian: - f''(x) ~= (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h^2) - This allows a larger step size h while maintaining accuracy (O(h^4) truncation error). + Per-electron local kinetic energy via finite differences on ``ln|Psi|``: + + T_L^(i) = -1/2 * ( nabla_i^2 ln|Psi| + |nabla_i ln|Psi||^2 ) + + Both the Laplacian and the gradient of ``ln|Psi|`` are computed with the + same 4-point 4th-order central stencil (sharing the four function + evaluations per coordinate): + + f''(x) ~= (-f(x+2h) + 16 f(x+h) - 30 f(x) + 16 f(x-h) - f(x-2h)) / (12 h^2) + f'(x) ~= ( -f(x+2h) + 8 f(x+h) - 8 f(x-h) + f(x-2h)) / (12 h) + + FD-on-``ln|Psi|`` (instead of FD-on-``Psi`` followed by 1/Psi division) + avoids the 1/Psi amplification of round-off near nuclear cusps and keeps + the achievable accuracy at the ~h^4 truncation floor of a smooth scalar + field. """ diff_h = 1.0e-3 # larger h is viable with 4th-order stencil - Psi = evaluate_wavefunction(wavefunction_data, r_up_carts, r_dn_carts) + ln_Psi_0 = evaluate_ln_wavefunction(wavefunction_data, r_up_carts, r_dn_carts) - def _eval_up(r_up): - return evaluate_wavefunction(wavefunction_data, r_up, r_dn_carts) + def _ln_eval_up(r_up): + return evaluate_ln_wavefunction(wavefunction_data, r_up, r_dn_carts) - def _eval_dn(r_dn): - return evaluate_wavefunction(wavefunction_data, r_up_carts, r_dn) + def _ln_eval_dn(r_dn): + return evaluate_ln_wavefunction(wavefunction_data, r_up_carts, r_dn) - def _fd4_second_deriv(eval_fn, r_carts, i, d, h): - """4th-order central FD for d^2f/dx^2.""" + def _fd4_first_and_second(eval_fn, r_carts, i, d, h): + """Return (df/dx, d^2 f/dx^2) from a 4-point 4th-order central stencil.""" r_p1 = r_carts.copy() r_p2 = r_carts.copy() r_m1 = r_carts.copy() @@ -1014,22 +1026,30 @@ def _fd4_second_deriv(eval_fn, r_carts, i, d, h): f_p2 = eval_fn(r_p2) f_m1 = eval_fn(r_m1) f_m2 = eval_fn(r_m2) - return (-f_p2 + 16 * f_p1 - 30 * Psi + 16 * f_m1 - f_m2) / (12 * h**2) + first = (-f_p2 + 8 * f_p1 - 8 * f_m1 + f_m2) / (12 * h) + second = (-f_p2 + 16 * f_p1 - 30 * ln_Psi_0 + 16 * f_m1 - f_m2) / (12 * h**2) + return first, second n_up, d_up = r_up_carts.shape - laplacian_Psi_up = np.zeros(n_up) + laplacian_ln_Psi_up = np.zeros(n_up) + grad_norm_sq_up = np.zeros(n_up) for i in range(n_up): for d in range(d_up): - laplacian_Psi_up[i] += _fd4_second_deriv(_eval_up, r_up_carts, i, d, diff_h) + g, lap = _fd4_first_and_second(_ln_eval_up, r_up_carts, i, d, diff_h) + laplacian_ln_Psi_up[i] += lap + grad_norm_sq_up[i] += g * g n_dn, d_dn = r_dn_carts.shape - laplacian_Psi_dn = np.zeros(n_dn) + laplacian_ln_Psi_dn = np.zeros(n_dn) + grad_norm_sq_dn = np.zeros(n_dn) for i in range(n_dn): for d in range(d_dn): - laplacian_Psi_dn[i] += _fd4_second_deriv(_eval_dn, r_dn_carts, i, d, diff_h) + g, lap = _fd4_first_and_second(_ln_eval_dn, r_dn_carts, i, d, diff_h) + laplacian_ln_Psi_dn[i] += lap + grad_norm_sq_dn[i] += g * g - kinetic_energy_all_elements_up = -1.0 / 2.0 * laplacian_Psi_up / Psi - kinetic_energy_all_elements_dn = -1.0 / 2.0 * laplacian_Psi_dn / Psi + kinetic_energy_all_elements_up = -0.5 * (laplacian_ln_Psi_up + grad_norm_sq_up) + kinetic_energy_all_elements_dn = -0.5 * (laplacian_ln_Psi_dn + grad_norm_sq_dn) return (kinetic_energy_all_elements_up, kinetic_energy_all_elements_dn) @@ -1965,18 +1985,15 @@ def _compute_nodal_distance_debug( r_up_carts: jax.Array, r_dn_carts: jax.Array, ) -> jax.Array: - r"""Compute the nodal distance using the paper's original formula (debug). - - Uses the definition from Eq. (2) of Pathak & Wagner (2020): + r"""Compute the nodal distance using autodiff of ``ln|Psi|`` (debug). - .. math:: - - \vec{x} = \frac{\Psi \, \nabla \Psi}{|\nabla \Psi|^2}, - - and returns :math:`|x|`. This is mathematically identical to - :func:`compute_nodal_distance` (:math:`1/|\nabla \ln|\Psi||`), but uses - :func:`evaluate_wavefunction` and automatic differentiation of :math:`\Psi` - instead of analytic :math:`\nabla \ln|\Psi|` derivatives. + Computes :math:`1 / |\nabla \ln|\Psi||` directly via + ``jax.grad(evaluate_ln_wavefunction)``. This is mathematically identical + to the paper's :math:`|x| = |\Psi| / |\nabla \Psi|` formula but avoids the + 1/|grad Psi| amplification that arises near the node from a Psi-side + autodiff path. As a result the debug path matches the analytic path + (which sums analytic :math:`\nabla J` and :math:`\nabla \ln |\det|`) + to ordinary autodiff round-off. Args: wavefunction_data: Wavefunction parameters (Jastrow + Geminal). @@ -1990,15 +2007,12 @@ def _compute_nodal_distance_debug( r_up = jnp.asarray(r_up_carts, dtype=dtype_jnp) r_dn = jnp.asarray(r_dn_carts, dtype=dtype_jnp) - Psi = evaluate_wavefunction(wavefunction_data, r_up, r_dn) - - grad_Psi_r_up = grad(evaluate_wavefunction, argnums=1)(wavefunction_data, r_up, r_dn) # (n_up, 3) - grad_Psi_r_dn = grad(evaluate_wavefunction, argnums=2)(wavefunction_data, r_up, r_dn) # (n_dn, 3) + grad_ln_Psi_up = grad(evaluate_ln_wavefunction, argnums=1)(wavefunction_data, r_up, r_dn) # (n_up, 3) + grad_ln_Psi_dn = grad(evaluate_ln_wavefunction, argnums=2)(wavefunction_data, r_up, r_dn) # (n_dn, 3) - grad_Psi_norm_sq = jnp.sum(grad_Psi_r_up**2) + jnp.sum(grad_Psi_r_dn**2) + grad_norm_sq = jnp.sum(grad_ln_Psi_up**2) + jnp.sum(grad_ln_Psi_dn**2) - # x_vec = Psi * grad_Psi / |grad_Psi|^2, so |x| = |Psi| / |grad_Psi| - return jnp.abs(Psi) / jnp.sqrt(grad_Psi_norm_sq) + return 1.0 / jnp.sqrt(grad_norm_sq) """ diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index 0833be1a..0a8609ed 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -59,7 +59,6 @@ _advance_kinetic_energy_all_elements_streaming_state, _compute_discretized_kinetic_energy_debug, _compute_kinetic_energy_all_elements_auto, - _compute_kinetic_energy_all_elements_debug, _compute_kinetic_energy_all_elements_fast_update_debug, _compute_kinetic_energy_auto, _compute_kinetic_energy_debug, @@ -196,100 +195,6 @@ def test_kinetic_energy_analytic_and_auto(trexio_file: str): np.testing.assert_allclose(K_analytic, K_auto, atol=atol, rtol=rtol) -@pytest.mark.activate_if_skip_heavy -@pytest.mark.numerical_diff -@pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) -def test_debug_and_auto_kinetic_energy_all_elements(trexio_file: str): - """Debug vs autodiff kinetic energy per-electron arrays. - - The debug path computes ``-1/2 * nabla^2Psi / Psi`` via central finite differences - on Psi (h = 2e-4); under mixed precision the fp32 round-off in ao_eval / - jastrow_eval propagates into Psi at ~1e-7 and is amplified by 1/h^2 = 2.5e7, - giving an O(1) relative error in the FD Laplacian. Marked ``numerical_diff`` - so conftest skips it under ``--precision-mode=mixed``. - """ - ( - _, - aos_data, - _, - _, - geminal_mo_data, - _, - ) = read_trexio_file( - trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), store_tuple=True - ) - jastrow_onebody_data = None - jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=1.0, jastrow_2b_type="exp") - jastrow_threebody_data = Jastrow_three_body_data.init_jastrow_three_body_data( - orb_data=aos_data, random_init=True, random_scale=1.0e-3 - ) - jastrow_data = Jastrow_data( - jastrow_one_body_data=jastrow_onebody_data, - jastrow_two_body_data=jastrow_twobody_data, - jastrow_three_body_data=jastrow_threebody_data, - ) - - wavefunction_data = Wavefunction_data(geminal_data=geminal_mo_data, jastrow_data=jastrow_data) - - num_ele_up = geminal_mo_data.num_electron_up - num_ele_dn = geminal_mo_data.num_electron_dn - rng = np.random.default_rng(42) - # Generate electron configuration away from wavefunction nodes to ensure - # numerical 2nd derivatives are well-conditioned (|Psi| >> 0). - from jqmc.wavefunction import evaluate_wavefunction - - for _ in range(200): - r_up_carts_np = rng.uniform(-2.0, 2.0, size=(num_ele_up, 3)) - r_dn_carts_np = rng.uniform(-2.0, 2.0, size=(num_ele_dn, 3)) - psi_val = evaluate_wavefunction(wavefunction_data, r_up_carts_np, r_dn_carts_np) - if abs(psi_val) > 1e-8: - break - else: - pytest.skip("Could not find electron configuration sufficiently far from node") - - r_up_carts_jnp = jnp.array(r_up_carts_np) - r_dn_carts_jnp = jnp.array(r_dn_carts_np) - - K_elements_up_debug, K_elements_dn_debug = _compute_kinetic_energy_all_elements_debug( - wavefunction_data=wavefunction_data, r_up_carts=r_up_carts_np, r_dn_carts=r_dn_carts_np - ) - K_elements_up_auto, K_elements_dn_auto = _compute_kinetic_energy_all_elements_auto( - wavefunction_data=wavefunction_data, r_up_carts=r_up_carts_jnp, r_dn_carts=r_dn_carts_jnp - ) - - # Debug path FDs Psi directly (2nd-order central FD with 1/Psi division), - # which is fundamentally rougher than FD on ln|Psi|. Near AE nuclear - # cusps (e.g. N all-electron) the high-order derivative growth combined - # with 1/Psi amplification puts the achievable accuracy below medium, - # so this test is held to loose tolerance as a documented exception - # to the "laplacian -> medium" rule. - atol, rtol = get_tolerance("wf_kinetic", "loose") - assert not np.any(np.isnan(np.asarray(K_elements_up_debug))), "NaN detected in first argument" - assert not np.any(np.isnan(np.asarray(K_elements_up_auto))), "NaN detected in second argument" - np.testing.assert_allclose(K_elements_up_debug, K_elements_up_auto, atol=atol, rtol=rtol) - assert not np.any(np.isnan(np.asarray(K_elements_dn_debug))), "NaN detected in first argument" - assert not np.any(np.isnan(np.asarray(K_elements_dn_auto))), "NaN detected in second argument" - np.testing.assert_allclose(K_elements_dn_debug, K_elements_dn_auto, atol=atol, rtol=rtol) - - assert not np.any(np.isnan(np.asarray(np.asarray(K_elements_up_debug)))), "NaN detected in first argument" - assert not np.any(np.isnan(np.asarray(np.asarray(K_elements_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(K_elements_up_debug), - np.asarray(K_elements_up_auto), - rtol=rtol, - atol=atol, - ) - - assert not np.any(np.isnan(np.asarray(np.asarray(K_elements_dn_debug)))), "NaN detected in first argument" - assert not np.any(np.isnan(np.asarray(np.asarray(K_elements_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(K_elements_dn_debug), - np.asarray(K_elements_dn_auto), - rtol=rtol, - atol=atol, - ) - - @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) def test_auto_and_analytic_kinetic_energy_all_elements(trexio_file: str): """Autodiff vs analytic kinetic energy per-electron arrays.""" @@ -614,12 +519,12 @@ def test_nodal_distance_analytic_vs_debug(trexio_file: str): ) # They should be identical up to numerical noise. - # Nodal distance ~ |Psi| / |grad Psi| amplifies any fp32 noise via the - # 1/|grad| factor near the node, so even the 'medium' tolerance is too - # tight in mixed mode. Use 'loose' here. + # Debug path now uses grad(ln|Psi|) directly (instead of grad(Psi)/Psi), + # so the 1/|grad Psi| amplification near the node is gone; medium + # tolerance suffices in both full and mixed precision. atol, rtol = get_tolerance_min( ("ao_eval", "jastrow_eval", "jastrow_grad_lap", "wf_kinetic"), - "loose", + "medium", ) np.testing.assert_allclose( np.asarray(nd_analytic), From d8cbcb1533340770241b605cd9c8f9f3e851dee0 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 21:35:32 +0900 Subject: [PATCH 53/97] Update tests: Reject configurations with |Psi| < TEST_NODE_AVOIDANCE_PSI_MIN in tests with 1/|grad ln|Psi|| near the node under mixed precision. --- jqmc/_setting.py | 6 +++++ tests/test_determinant.py | 3 ++- tests/test_wave_function.py | 45 ++++++++++++++++++++++++------------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/jqmc/_setting.py b/jqmc/_setting.py index ef6ea535..05f66d14 100644 --- a/jqmc/_setting.py +++ b/jqmc/_setting.py @@ -74,6 +74,12 @@ atol_consistency = 1.0e-8 rtol_consistency = 1.0e-6 +# Minimum |Psi| (or |det|) below which an electron configuration is considered +# "near a wavefunction node" and rejected by tests that compare quantities +# involving 1/|Psi|, grad ln|Psi|, or 2nd derivatives via finite differences. +# Centralized so all node-avoidance loops share the same threshold. +TEST_NODE_AVOIDANCE_PSI_MIN = 1.0e-8 + # --- Test tolerance dict (dtype-aware) --- # # Accessed via ``_precision.get_tolerance(zone, level)`` which resolves the diff --git a/tests/test_determinant.py b/tests/test_determinant.py index ee5cc6b6..85dc44a5 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -47,6 +47,7 @@ sys.path.insert(0, project_root) from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc._setting import TEST_NODE_AVOIDANCE_PSI_MIN from jqmc.atomic_orbital import AOs_sphe_data, compute_overlap_matrix from jqmc.determinant import ( Geminal_data, @@ -1116,7 +1117,7 @@ def _generate_config(): for _ in range(500): r_up_carts, r_dn_carts = _generate_config() det_val = compute_det_geminal_all_elements(geminal_data=geminal_ao_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - if abs(det_val) > 1e-8: + if abs(det_val) > TEST_NODE_AVOIDANCE_PSI_MIN: break else: pytest.skip("Could not find electron configuration sufficiently far from determinant node") diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index 0a8609ed..395178da 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -46,6 +46,7 @@ sys.path.insert(0, project_root) from jqmc._precision import get_tolerance, get_tolerance_min +from jqmc._setting import TEST_NODE_AVOIDANCE_PSI_MIN from jqmc.determinant import compute_geminal_all_elements from jqmc.jastrow_factor import ( Jastrow_data, @@ -126,7 +127,7 @@ def test_kinetic_energy_analytic_and_numerical(trexio_file: str): r_up_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_up, 3) + r_cart_min r_dn_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_dn, 3) + r_cart_min psi_val = evaluate_wavefunction(wavefunction_data, r_up_carts, r_dn_carts) - if abs(psi_val) > 1e-8: + if abs(psi_val) > TEST_NODE_AVOIDANCE_PSI_MIN: break else: pytest.skip("Could not find electron configuration sufficiently far from node") @@ -497,19 +498,31 @@ def test_nodal_distance_analytic_vs_debug(trexio_file: str): num_ele_up = geminal_mo_data.num_electron_up num_ele_dn = geminal_mo_data.num_electron_dn r_cart_min, r_cart_max = -3.0, +3.0 - np.random.seed(42) - r_up_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_up, 3) + r_cart_min - r_dn_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_dn, 3) + r_cart_min - r_up_carts_jnp = jnp.asarray(r_up_carts) - r_dn_carts_jnp = jnp.asarray(r_dn_carts) + # Reject configurations near the wavefunction node: when |Psi| ~ 0 the + # quantity |grad ln|Psi|| diverges, which makes the analytic vs autodiff + # comparison sensitive to floating-point round-off (a few percent in + # mixed precision). Same node-avoidance pattern as other tests. + from jqmc.wavefunction import evaluate_wavefunction - # Analytic path - nd_analytic = compute_nodal_distance( - wavefunction_data=wavefunction_data, - r_up_carts=r_up_carts_jnp, - r_dn_carts=r_dn_carts_jnp, - ) + rng = np.random.default_rng(42) + nd_analytic = None + r_up_carts_jnp = r_dn_carts_jnp = None + for _ in range(50): + r_up_carts = (r_cart_max - r_cart_min) * rng.random((num_ele_up, 3)) + r_cart_min + r_dn_carts = (r_cart_max - r_cart_min) * rng.random((num_ele_dn, 3)) + r_cart_min + r_up_carts_jnp = jnp.asarray(r_up_carts) + r_dn_carts_jnp = jnp.asarray(r_dn_carts) + psi_val = evaluate_wavefunction(wavefunction_data, r_up_carts_jnp, r_dn_carts_jnp) + if abs(float(psi_val)) > TEST_NODE_AVOIDANCE_PSI_MIN: + nd_analytic = compute_nodal_distance( + wavefunction_data=wavefunction_data, + r_up_carts=r_up_carts_jnp, + r_dn_carts=r_dn_carts_jnp, + ) + break + else: + pytest.skip(f"Could not find electron configuration sufficiently far from node for {trexio_file}") # Debug path (paper formula) nd_debug = _compute_nodal_distance_debug( @@ -518,10 +531,10 @@ def test_nodal_distance_analytic_vs_debug(trexio_file: str): r_dn_carts=r_dn_carts_jnp, ) - # They should be identical up to numerical noise. - # Debug path now uses grad(ln|Psi|) directly (instead of grad(Psi)/Psi), - # so the 1/|grad Psi| amplification near the node is gone; medium - # tolerance suffices in both full and mixed precision. + # Both paths are autodiff/analytic (no FD), so under medium tolerance the + # only source of disagreement is fp32 round-off in the mixed-precision + # build. Avoiding the node keeps |grad ln|Psi|| bounded and that + # round-off well within medium rtol. atol, rtol = get_tolerance_min( ("ao_eval", "jastrow_eval", "jastrow_grad_lap", "wf_kinetic"), "medium", From 5d84d39314aa8bd36deda03bdd4dfd0ddf4052fc Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 5 May 2026 23:08:51 +0900 Subject: [PATCH 54/97] Remove test_kinetic_energy_analytic_and_numerical and test_numerial_and_auto_grads_and_laplacians_ln_Det because the numerical ones are intrinsically unstable and already the analytical ones are validated with auto-grad ones. --- jqmc/_setting.py | 6 -- tests/test_determinant.py | 122 +----------------------------------- tests/test_wave_function.py | 80 +++++------------------ 3 files changed, 17 insertions(+), 191 deletions(-) diff --git a/jqmc/_setting.py b/jqmc/_setting.py index 05f66d14..ef6ea535 100644 --- a/jqmc/_setting.py +++ b/jqmc/_setting.py @@ -74,12 +74,6 @@ atol_consistency = 1.0e-8 rtol_consistency = 1.0e-6 -# Minimum |Psi| (or |det|) below which an electron configuration is considered -# "near a wavefunction node" and rejected by tests that compare quantities -# involving 1/|Psi|, grad ln|Psi|, or 2nd derivatives via finite differences. -# Centralized so all node-avoidance loops share the same threshold. -TEST_NODE_AVOIDANCE_PSI_MIN = 1.0e-8 - # --- Test tolerance dict (dtype-aware) --- # # Accessed via ``_precision.get_tolerance(zone, level)`` which resolves the diff --git a/tests/test_determinant.py b/tests/test_determinant.py index 85dc44a5..b8fed1ba 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -47,7 +47,6 @@ sys.path.insert(0, project_root) from jqmc._precision import get_tolerance, get_tolerance_min -from jqmc._setting import TEST_NODE_AVOIDANCE_PSI_MIN from jqmc.atomic_orbital import AOs_sphe_data, compute_overlap_matrix from jqmc.determinant import ( Geminal_data, @@ -57,7 +56,6 @@ _compute_geminal_all_elements, _compute_geminal_all_elements_debug, _compute_grads_and_laplacian_ln_Det_auto, - _compute_grads_and_laplacian_ln_Det_debug, _compute_grads_and_laplacian_ln_Det_fast_debug, _compute_ratio_determinant_part_debug, _compute_ratio_determinant_part_rank1_update, @@ -1057,123 +1055,6 @@ def test_one_row_or_one_column_update(trexio_file: str): ) -@pytest.mark.numerical_diff -@pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) -def test_numerial_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): - """Test the numerical and automatic gradients of the logarithm of the determinant of the geminal wave function.""" - ( - structure_data, - aos_data, - mos_data_up, - mos_data_dn, - geminal_mo_data, - coulomb_potential_data, - ) = read_trexio_file( - trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), - store_tuple=True, - ) - - geminal_mo_data.sanity_check() - - num_electron_up = geminal_mo_data.num_electron_up - num_electron_dn = geminal_mo_data.num_electron_dn - - if coulomb_potential_data.ecp_flag: - charges = np.array(structure_data.atomic_numbers) - np.array(coulomb_potential_data.z_cores) - else: - charges = np.array(structure_data.atomic_numbers) - - coords = structure_data._positions_cart_np - - geminal_ao_data = Geminal_data.convert_from_MOs_to_AOs(geminal_mo_data) - geminal_ao_data.sanity_check() - - # Generate electron configuration far from determinant nodes so that - # numerical 2nd derivatives are well-conditioned. - def _generate_config(): - r_up = [] - r_dn = [] - for i in range(len(coords)): - charge = charges[i] - num_electrons = int(np.round(charge)) - x, y, z = coords[i] - for _ in range(num_electrons): - distance = np.random.uniform(0.5 / max(charge, 1), 1.5 / max(charge, 1)) - theta = np.random.uniform(0, np.pi) - phi = np.random.uniform(0, 2 * np.pi) - dx = distance * np.sin(theta) * np.cos(phi) - dy = distance * np.sin(theta) * np.sin(phi) - dz = distance * np.cos(theta) - if len(r_up) < num_electron_up: - r_up.append(np.array([x + dx, y + dy, z + dz])) - else: - r_dn.append(np.array([x + dx, y + dy, z + dz])) - for _ in range(num_electron_up - len(r_up)): - r_up.append(np.random.choice(coords) + np.random.normal(scale=0.2, size=3)) - for _ in range(num_electron_dn - len(r_dn)): - r_dn.append(np.random.choice(coords) + np.random.normal(scale=0.2, size=3)) - return np.array(r_up).reshape(-1, 3), np.array(r_dn).reshape(-1, 3) - - for _ in range(500): - r_up_carts, r_dn_carts = _generate_config() - det_val = compute_det_geminal_all_elements(geminal_data=geminal_ao_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - if abs(det_val) > TEST_NODE_AVOIDANCE_PSI_MIN: - break - else: - pytest.skip("Could not find electron configuration sufficiently far from determinant node") - - grad_ln_D_up_numerical, grad_ln_D_dn_numerical, lap_ln_D_up_numerical, lap_ln_D_dn_numerical = ( - _compute_grads_and_laplacian_ln_Det_debug( - geminal_data=geminal_ao_data, - r_up_carts=r_up_carts, - r_dn_carts=r_dn_carts, - ) - ) - - grad_ln_D_up_auto, grad_ln_D_dn_auto, lap_ln_D_up_auto, lap_ln_D_dn_auto = _compute_grads_and_laplacian_ln_Det_auto( - geminal_data=geminal_ao_data, - r_up_carts=r_up_carts, - r_dn_carts=r_dn_carts, - ) - - atol_g, rtol_g = get_tolerance("det_grad_lap", "strict") - atol_l, rtol_l = get_tolerance("det_grad_lap", "medium") - assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_numerical)))), "NaN detected in first argument" - assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_ln_D_up_numerical), - np.asarray(grad_ln_D_up_auto), - atol=atol_g, - rtol=rtol_g, - ) - assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_dn_numerical)))), "NaN detected in first argument" - assert not np.any(np.isnan(np.asarray(np.asarray(grad_ln_D_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(grad_ln_D_dn_numerical), - np.asarray(grad_ln_D_dn_auto), - atol=atol_g, - rtol=rtol_g, - ) - assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_up_numerical)))), "NaN detected in first argument" - assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_up_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(lap_ln_D_up_numerical), - np.asarray(lap_ln_D_up_auto), - rtol=rtol_l, - atol=atol_l, - ) - assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_dn_numerical)))), "NaN detected in first argument" - assert not np.any(np.isnan(np.asarray(np.asarray(lap_ln_D_dn_auto)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(lap_ln_D_dn_numerical), - np.asarray(lap_ln_D_dn_auto), - rtol=rtol_l, - atol=atol_l, - ) - - jax.clear_caches() - - @pytest.mark.activate_if_skip_heavy @pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvdz_cart.h5", "H_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) def test_analytic_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): @@ -1195,6 +1076,9 @@ def test_analytic_and_auto_grads_and_laplacians_ln_Det(trexio_file: str): num_electron_up = geminal_mo_data.num_electron_up num_electron_dn = geminal_mo_data.num_electron_dn + # Seed RNG for determinism (CI must not depend on previous tests' draws). + np.random.seed(42) + # Initialization r_up_carts = [] r_dn_carts = [] diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index 395178da..59caa4e2 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -46,7 +46,6 @@ sys.path.insert(0, project_root) from jqmc._precision import get_tolerance, get_tolerance_min -from jqmc._setting import TEST_NODE_AVOIDANCE_PSI_MIN from jqmc.determinant import compute_geminal_all_elements from jqmc.jastrow_factor import ( Jastrow_data, @@ -62,7 +61,6 @@ _compute_kinetic_energy_all_elements_auto, _compute_kinetic_energy_all_elements_fast_update_debug, _compute_kinetic_energy_auto, - _compute_kinetic_energy_debug, _compute_nodal_distance_debug, _init_kinetic_energy_all_elements_streaming_state, _kinetic_energy_from_streaming_state, @@ -82,13 +80,11 @@ jax.config.update("jax_traceback_filtering", "off") -@pytest.mark.activate_if_skip_heavy -@pytest.mark.numerical_diff @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) -def test_kinetic_energy_analytic_and_numerical(trexio_file: str): - """Test the kinetic energy computation.""" +def test_kinetic_energy_analytic_and_auto(trexio_file: str): + """Compare analytic and autodiff kinetic energy implementations.""" ( - structure_data, + _, aos_data, _, _, @@ -99,85 +95,37 @@ def test_kinetic_energy_analytic_and_numerical(trexio_file: str): ) jastrow_onebody_data = None - jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="exp") + jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=1.0, jastrow_2b_type="pade") jastrow_threebody_data = Jastrow_three_body_data.init_jastrow_three_body_data( orb_data=aos_data, random_init=True, random_scale=1.0e-3 ) - jastrow_nn_data = Jastrow_NN_data.init_from_structure(structure_data=structure_data, hidden_dim=5, num_layers=2, cutoff=5.0) - jastrow_data = Jastrow_data( jastrow_one_body_data=jastrow_onebody_data, jastrow_two_body_data=jastrow_twobody_data, jastrow_three_body_data=jastrow_threebody_data, - jastrow_nn_data=jastrow_nn_data, ) - jastrow_data.sanity_check() wavefunction_data = Wavefunction_data(geminal_data=geminal_mo_data, jastrow_data=jastrow_data) - wavefunction_data.sanity_check() num_ele_up = geminal_mo_data.num_electron_up num_ele_dn = geminal_mo_data.num_electron_dn r_cart_min, r_cart_max = -2.0, +2.0 - # Generate electron configuration away from wavefunction nodes to ensure - # numerical 2nd derivatives are well-conditioned (|Psi| >> 0). + + # Reject configurations near the wavefunction node: 1/|Psi| amplification + # near the node breaks the analytic-vs-auto comparison under fp32. from jqmc.wavefunction import evaluate_wavefunction - for _ in range(200): - r_up_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_up, 3) + r_cart_min - r_dn_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_dn, 3) + r_cart_min + rng = np.random.default_rng(42) + r_up_carts = r_dn_carts = None + for _ in range(50): + r_up_carts = (r_cart_max - r_cart_min) * rng.random((num_ele_up, 3)) + r_cart_min + r_dn_carts = (r_cart_max - r_cart_min) * rng.random((num_ele_dn, 3)) + r_cart_min psi_val = evaluate_wavefunction(wavefunction_data, r_up_carts, r_dn_carts) - if abs(psi_val) > TEST_NODE_AVOIDANCE_PSI_MIN: + if abs(float(psi_val)) > 1.0e-8: break else: pytest.skip("Could not find electron configuration sufficiently far from node") - K_debug = _compute_kinetic_energy_debug(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - K_jax = compute_kinetic_energy(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) - atol, rtol = get_tolerance("wf_kinetic", "medium") - assert not np.any(np.isnan(np.asarray(np.asarray(K_debug)))), "NaN detected in first argument" - assert not np.any(np.isnan(np.asarray(np.asarray(K_jax)))), "NaN detected in second argument" - np.testing.assert_allclose( - np.asarray(K_debug), - np.asarray(K_jax), - rtol=rtol, - atol=atol, - ) - - -@pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"]) -def test_kinetic_energy_analytic_and_auto(trexio_file: str): - """Compare analytic and autodiff kinetic energy implementations.""" - ( - _, - aos_data, - _, - _, - geminal_mo_data, - _, - ) = read_trexio_file( - trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), store_tuple=True - ) - - jastrow_onebody_data = None - jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=1.0, jastrow_2b_type="pade") - jastrow_threebody_data = Jastrow_three_body_data.init_jastrow_three_body_data( - orb_data=aos_data, random_init=True, random_scale=1.0e-3 - ) - jastrow_data = Jastrow_data( - jastrow_one_body_data=jastrow_onebody_data, - jastrow_two_body_data=jastrow_twobody_data, - jastrow_three_body_data=jastrow_threebody_data, - ) - - wavefunction_data = Wavefunction_data(geminal_data=geminal_mo_data, jastrow_data=jastrow_data) - - num_ele_up = geminal_mo_data.num_electron_up - num_ele_dn = geminal_mo_data.num_electron_dn - r_cart_min, r_cart_max = -2.0, +2.0 - r_up_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_up, 3) + r_cart_min - r_dn_carts = (r_cart_max - r_cart_min) * np.random.rand(num_ele_dn, 3) + r_cart_min - K_analytic = compute_kinetic_energy(wavefunction_data=wavefunction_data, r_up_carts=r_up_carts, r_dn_carts=r_dn_carts) K_auto = _compute_kinetic_energy_auto( wavefunction_data=wavefunction_data, @@ -514,7 +462,7 @@ def test_nodal_distance_analytic_vs_debug(trexio_file: str): r_up_carts_jnp = jnp.asarray(r_up_carts) r_dn_carts_jnp = jnp.asarray(r_dn_carts) psi_val = evaluate_wavefunction(wavefunction_data, r_up_carts_jnp, r_dn_carts_jnp) - if abs(float(psi_val)) > TEST_NODE_AVOIDANCE_PSI_MIN: + if abs(float(psi_val)) > 1.0e-8: nd_analytic = compute_nodal_distance( wavefunction_data=wavefunction_data, r_up_carts=r_up_carts_jnp, From fad83aa24cfe9392cb21a078b7021b4fb95b5506 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 7 May 2026 00:34:54 +0900 Subject: [PATCH 55/97] Drastic speed-up: stream cached AO + paired tables to all det-ratio/jas-ratio hot paths Eliminate the redundant `lambda_paired @ ao_dn` precontract and bulk-side AO evaluations that were recomputed at every single-electron move. A new slim streaming state `Det_ratio_streaming_state` (4 fields: ao_up, ao_dn, paired_dn, paired_up_lambda) is built once per MCMC chain entry and refreshed one column/row per accepted single-electron move (no rank-1 drift), then forwarded to every ratio-style kernel: - MCMC wf-update (jqmc_mcmc.py): `_update_electron_positions` and the only-up variant carry the slim state and use new helpers `_compute_v_up_move_from_det_ratio_state` / `_compute_u_dn_move_from_det_ratio_state` in place of the twice-called `compute_geminal_{up_one_row,dn_one_column}_elements` pattern. - MCMC measurement (e_L) chain: `compute_local_energy_fast` -> `compute_coulomb_potential_fast` -> `compute_ecp_coulomb_potential_fast` -> `compute_ecp_non_local_parts_nearest_neighbors_fast_update`. - LRDMC discretized kinetic mesh: `compute_discretized_kinetic_energy_fast_update` forwards the state to `_compute_ratio_determinant_part_split_spin`. - LRDMC ECP non-local mesh: same kernel, second consumer. - GFMC projection inv-update: `_body_step_core` (GFMC_n) and `_projection_t_core` (GFMC_t) thread `kinetic_state.det_state` into the Sherman-Morrison v / u construction. - Eliminate redundant J3 AO eval in discretized-kinetic Jastrow ratio The existing `Det_streaming_state` (with grad/lap fields, used by the LRDMC continuum kinetic path) gains a single new field `paired_up_lambda`, making it a structural superset of the slim state and consumable by every ratio kernel via duck-typing. Streaming dispatch in GFMC is relaxed to `use_streaming = jastrow_nn_data is None`; the previous additional requirement that a three-body Jastrow be present is dropped, since the determinant streaming path is now beneficial on its own. --- jqmc/coulomb_potential.py | 20 +++ jqmc/determinant.py | 289 ++++++++++++++++++++++++++++++++-- jqmc/hamiltonians.py | 6 + jqmc/jastrow_factor.py | 15 +- jqmc/jqmc_gfmc.py | 160 ++++++++++++------- jqmc/jqmc_mcmc.py | 319 ++++++++++++++++++++++++++++++-------- jqmc/wavefunction.py | 8 + tests/test_determinant.py | 183 ++++++++++++++++++++++ tests/test_ecps.py | 94 ++++++++++- 9 files changed, 957 insertions(+), 137 deletions(-) diff --git a/jqmc/coulomb_potential.py b/jqmc/coulomb_potential.py index 36007951..bc646920 100644 --- a/jqmc/coulomb_potential.py +++ b/jqmc/coulomb_potential.py @@ -1485,6 +1485,7 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( Nv: int = Nv_default, flag_determinant_only: bool = False, j3_state: "Jastrow_three_body_streaming_state | None" = None, + det_ratio_state=None, ) -> tuple[list, list, list, float]: """Fast-update variant of non-local ECP contributions (nearest neighbors). @@ -1507,6 +1508,12 @@ def compute_ecp_non_local_parts_nearest_neighbors_fast_update( recomputation. Use the value carried in the projection's ``Kinetic_streaming_state.j3_state``; pass ``None`` (default) for the original 1-shot path used by observation/MCMC code. + det_ratio_state: Optional :class:`Det_ratio_streaming_state` (or a + superset like :class:`Det_streaming_state`) consistent with + ``(r_up_carts, r_dn_carts)``. Forwarded to + ``_compute_ratio_determinant_part_split_spin`` so it can skip + the bulk-side AO eval and the two ``lambda_paired @ ao_*`` + precontracts. Returns: tuple[list[jax.Array], list[jax.Array], jax.Array, float]: @@ -1667,6 +1674,7 @@ def _V_l_mapped(rel, ang_mom, exponent, coefficient, power): old_r_dn_carts=r_dn_carts, new_r_up_shifted=up_mesh_r_up, new_r_dn_shifted=dn_mesh_r_dn, + det_ratio_state=det_ratio_state, ) # Cast determinant/Jastrow ratio terms to the local coulomb zone dtype # before downstream contractions; avoid relying on implicit promotion. @@ -2165,6 +2173,7 @@ def compute_ecp_coulomb_potential_fast( A_old_inv: jax.Array, NN: int = NN_default, Nv: int = Nv_default, + det_ratio_state=None, ) -> float: """Compute total ECP energy (local + non-local) using a pre-computed geminal inverse. @@ -2183,6 +2192,9 @@ def compute_ecp_coulomb_potential_fast( A_old_inv (jax.Array): Pre-computed inverse of the reference geminal matrix. NN (int): Number of nearest nuclei to include for each electron in the non-local term. Nv (int): Number of quadrature points on the sphere. + det_ratio_state: Optional :class:`Det_ratio_streaming_state` (or + superset). Forwarded to the non-local ECP ratio kernel so it can + skip the bulk-side AO eval and ``lambda @ ao_*`` precontracts. Returns: float: Sum of local and non-local ECP contributions for the given geometry. @@ -2213,6 +2225,7 @@ def compute_ecp_coulomb_potential_fast( NN=NN, Nv=Nv, flag_determinant_only=False, + det_ratio_state=det_ratio_state, ) V_ecp = ecp_local_parts + ecp_nonlocal_parts @@ -2700,6 +2713,7 @@ def compute_coulomb_potential_fast( NN: int = NN_default, Nv: int = Nv_default, wavefunction_data: Wavefunction_data = None, + det_ratio_state=None, ) -> float: """Compute total Coulomb energy using a pre-computed geminal inverse for ECP non-local terms. @@ -2718,6 +2732,11 @@ def compute_coulomb_potential_fast( NN (int): Number of nearest nuclei to include for each electron in the non-local term. Nv (int): Number of quadrature points on the sphere. wavefunction_data (Wavefunction_data): Wavefunction (geminal + Jastrow) used for ECP ratios; required when ``ecp_flag`` is True. + det_ratio_state: Optional :class:`Det_ratio_streaming_state` (or + superset) consistent with ``(r_up_carts, r_dn_carts)``. + Forwarded to :func:`compute_ecp_coulomb_potential_fast` so the + non-local ECP ratio kernel can skip the bulk-side AO eval and + ``lambda @ ao_*`` precontracts. Returns: float: Sum of bare Coulomb (ion-ion, electron-ion, electron-electron) and ECP (local + non-local) energies. @@ -2755,6 +2774,7 @@ def compute_coulomb_potential_fast( A_old_inv=A_old_inv, NN=NN, Nv=Nv, + det_ratio_state=det_ratio_state, ) return bare_coulomb_potential + ecp_coulomb_potential diff --git a/jqmc/determinant.py b/jqmc/determinant.py index 572d214c..aca78cb9 100755 --- a/jqmc/determinant.py +++ b/jqmc/determinant.py @@ -1656,6 +1656,7 @@ def _compute_ratio_determinant_part_split_spin( old_r_dn_carts: jax.Array, new_r_up_shifted: jax.Array, new_r_dn_shifted: jax.Array, + det_ratio_state=None, ) -> jax.Array: r"""Determinant ratio for a block-structured mesh where up and dn electrons move separately. @@ -1673,6 +1674,14 @@ def _compute_ratio_determinant_part_split_spin( up electron differs from ``old_r_up_carts`` per config. new_r_dn_shifted: Dn-block proposed coords ``(G_dn, N_dn, 3)``. Exactly one dn electron differs from ``old_r_dn_carts`` per config. + det_ratio_state: Optional :class:`Det_ratio_streaming_state` (or a + superset like :class:`Det_streaming_state`) that is consistent + with ``(old_r_up_carts, old_r_dn_carts)``. When provided, this + function reuses ``state.ao_up``, ``state.ao_dn``, + ``state.paired_dn``, and ``state.paired_up_lambda`` to skip the + heavy ``compute_orb_api`` calls for the bulk old configuration + and the two ``lambda_paired @ ao_*`` precontracts. Pass ``None`` + (default) for the legacy path used by debug / observation code. Returns: jax.Array: Concatenated determinant ratios ``(G_up + G_dn,)``. @@ -1696,6 +1705,8 @@ def _compute_ratio_determinant_part_split_spin( num_dn = old_r_dn_carts.shape[0] # Degenerate cases fall back to the general function on empty slices. + # (Slim state plumb is irrelevant here -- those code paths are not in the + # production hot loop.) if num_up == 0 or num_dn == 0: combined_up = jnp.concatenate( [new_r_up_shifted, jnp.broadcast_to(old_r_up_carts[None], (new_r_dn_shifted.shape[0], num_up, 3))], @@ -1715,11 +1726,25 @@ def _compute_ratio_determinant_part_split_spin( lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) - # Precompute old AO matrices once. Explicitly upcast to the geminal zone - # (compute_orb_api may return ao_eval / mo_eval dtype, e.g. fp32 for AGP) to avoid - # relying on JAX implicit type promotion in the lambda matmuls below. - orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts).astype(dtype_jnp) - orb_matrix_dn_old = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, old_r_dn_carts).astype(dtype_jnp) + if det_ratio_state is None: + # Legacy path: evaluate old-side AO matrices and precontracts from scratch. + # Explicitly upcast to the det_ratio zone (compute_orb_api may return + # ao_eval / mo_eval dtype, e.g. fp32 for AGP) to avoid relying on JAX + # implicit type promotion in the lambda matmuls below. + orb_matrix_dn_old = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, old_r_dn_carts).astype(dtype_jnp) + orb_matrix_up_old = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, old_r_up_carts).astype(dtype_jnp) + # M_paired_up := lambda_paired @ orb_dn_old (n_orb_up, N_dn) + M_paired_up = jnp.dot(lambda_matrix_paired, orb_matrix_dn_old) + # M_paired_dn := orb_up_old.T @ lambda_paired (N_up, n_orb_dn) + M_paired_dn = jnp.dot(orb_matrix_up_old.T, lambda_matrix_paired) + else: + # Streaming path: reuse cached AO + paired tables maintained by the + # walker. Cast at use site to the det_ratio zone (Principle 3b). + # state.paired_dn and state.paired_up_lambda are mathematically equal + # to lambda_paired @ ao_dn_old and ao_up_old.T @ lambda_paired + # respectively (refreshed on every accepted single-electron move). + M_paired_up = jnp.asarray(det_ratio_state.paired_dn, dtype=dtype_jnp) + M_paired_dn = jnp.asarray(det_ratio_state.paired_up_lambda, dtype=dtype_jnp) # --- UP BLOCK: up electron moved, dn unchanged ----------------------------- delta_up = new_r_up_shifted - old_r_up_carts # (G_up, N_up, 3) @@ -1739,7 +1764,6 @@ def _compute_ratio_determinant_part_split_spin( orb_up_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_new_flat).astype( dtype_jnp ) # (n_orb_up, G_up) - M_paired_up = jnp.dot(lambda_matrix_paired, orb_matrix_dn_old) # (n_orb_up, N_dn) row_paired = jnp.dot(orb_up_new_batch.T, M_paired_up) # (G_up, N_dn) row_unpaired = jnp.dot(orb_up_new_batch.T, lambda_matrix_unpaired) # (G_up, num_unpaired) new_rows_up = jnp.hstack([row_paired, row_unpaired]) # (G_up, N_up) @@ -1761,7 +1785,6 @@ def _compute_ratio_determinant_part_split_spin( orb_dn_new_batch = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_new_flat).astype( dtype_jnp ) # (n_orb_dn, G_dn) - M_paired_dn = jnp.dot(orb_matrix_up_old.T, lambda_matrix_paired) # (N_up, n_orb_dn) new_cols_dn = jnp.dot(M_paired_dn, orb_dn_new_batch).T # (G_dn, N_up) A_row_for_dn = jnp.take(A_old_inv_z, idx_dn, axis=0) # (G_dn, N_up) @@ -2243,8 +2266,6 @@ def compute_grads_and_laplacian_ln_Det_fast( # (c) the full geminal grad/lap matrices, all consistent with the current # (r_up, r_dn). A single-electron move advances them in O(n_ao^2 + n_ao*N_e # + N_e^2) per call, vs. O(n_ao^2*N_e + n_ao*N_e^2) for fresh recompute. -# -# See ``lrdmc_refactoring.md`` Section 1-3 for the field list / advance derivation. # --------------------------------------------------------------------------- @@ -2253,7 +2274,12 @@ class Det_streaming_state: """Auxiliary tables required to evaluate ``nablaln|Det|`` / ``nabla^2ln|Det|`` incrementally under single-electron moves. - See ``lrdmc_refactoring.md`` Section 1-3 for the per-field rationale. + The field ``paired_up_lambda`` makes this state a structural superset of + :class:`Det_ratio_streaming_state`, so it can be consumed directly by + ratio kernels (``_compute_ratio_determinant_part_split_spin`` and + friends) via duck-typing. The four "ratio fields" (``ao_up``, + ``ao_dn``, ``paired_dn``, ``paired_up_lambda``) carry the same invariants + as the slim state. """ ao_up: jax.Array @@ -2265,6 +2291,7 @@ class Det_streaming_state: paired_dn: jax.Array paired_dn_grads: jax.Array paired_dn_lap: jax.Array + paired_up_lambda: jax.Array # (N_up, n_ao_dn) = ao_up.T @ lambda_paired geminal_grad_up: jax.Array geminal_grad_dn: jax.Array geminal_lap_up: jax.Array @@ -2354,6 +2381,11 @@ def _init_grads_laplacian_ln_Det_streaming_state( paired_dn_grads = jnp.einsum("ab,gbn->gan", lambda_matrix_paired, ao_dn_grads) paired_dn_lap = lambda_matrix_paired @ ao_dn_lap + # ao_up.T @ lambda_paired -- the dn-block precontract used by ratio + # kernels. Refreshed (not rank-1 advanced) on up-move below; unchanged + # on dn-move because it depends on ao_up only. + paired_up_lambda = ao_up.T @ lambda_matrix_paired + # Phase 2: full geminal grad/lap matrices (paired || unpaired hstack'd). geminal_grad_up_paired = jnp.einsum("gia,aj->gij", jnp.swapaxes(ao_up_grads, 1, 2), paired_dn) geminal_grad_up_unpaired = jnp.einsum("gia,ak->gik", jnp.swapaxes(ao_up_grads, 1, 2), lambda_matrix_unpaired) @@ -2398,6 +2430,7 @@ def _init_grads_laplacian_ln_Det_streaming_state( paired_dn=paired_dn, paired_dn_grads=paired_dn_grads, paired_dn_lap=paired_dn_lap, + paired_up_lambda=paired_up_lambda, geminal_grad_up=geminal_grad_up, geminal_grad_dn=geminal_grad_dn, geminal_lap_up=geminal_lap_up, @@ -2453,6 +2486,11 @@ def _branch_up(_): new_ao_up_grads = state.ao_up_grads.at[:, :, moved_index].set(grad_col) new_ao_up_lap = state.ao_up_lap.at[:, moved_index].set(lap_col) + # --- paired_up_lambda row: ao_col @ lambda_paired -> (n_ao_dn,) + # depends on ao_up only, so on dn-move it stays unchanged. + new_paired_up_lambda_row = jnp.dot(ao_col, lambda_matrix_paired) + new_paired_up_lambda = state.paired_up_lambda.at[moved_index, :].set(new_paired_up_lambda_row) + # --- Phase 2: row k of geminal_* (paired || unpaired) -------------- # row of geminal_grad_up: einsum("ga,aj->gj", grad_col, paired_dn) || # einsum("ga,ak->gk", grad_col, lambda_u) @@ -2493,6 +2531,7 @@ def _branch_up(_): ao_up=new_ao_up, ao_up_grads=new_ao_up_grads, ao_up_lap=new_ao_up_lap, + paired_up_lambda=new_paired_up_lambda, geminal_grad_up=new_geminal_grad_up, geminal_grad_dn=new_geminal_grad_dn, geminal_lap_up=new_geminal_lap_up, @@ -2578,6 +2617,236 @@ def _branch_dn(_): return jax.lax.cond(moved_spin_is_up, _branch_up, _branch_dn, operand=None) +# --------------------------------------------------------------------------- +# Det_ratio_streaming_state -- slim, value-only streaming state for ratio +# kernels (MCMC walk body, GFMC inv-update, LRDMC mesh ratio kernels). +# +# Compared with Det_streaming_state above, this slim variant carries only +# the four fields needed for ratio computation; no grad/lap fields. +# +# Invariants maintained at every accepted single-electron move: +# ao_up == compute_orb_api(orb_data_up_spin, r_up_carts) +# ao_dn == compute_orb_api(orb_data_dn_spin, r_dn_carts) +# paired_dn == lambda_matrix_paired @ ao_dn +# paired_up_lambda == ao_up.T @ lambda_matrix_paired +# +# All four fields are *refreshed* (not rank-1 advanced) on accept: when an +# electron moves, the corresponding column or row is recomputed from the +# fresh AO at the new position. This means drift accumulation does not +# occur in the slim state itself. +# --------------------------------------------------------------------------- + + +@struct.dataclass +class Det_ratio_streaming_state: + """Value-only AO + paired tables for ratio kernels. + + Fields: + ao_up: ``(n_ao_up, N_up)`` -- per-electron AO evaluations for up spin. + ao_dn: ``(n_ao_dn, N_dn)`` -- per-electron AO evaluations for dn spin. + paired_dn: ``(n_ao_up, N_dn)`` -- precomputed ``lambda_paired @ ao_dn``. + paired_up_lambda: ``(N_up, n_ao_dn)`` -- precomputed ``ao_up.T @ lambda_paired``. + """ + + ao_up: jax.Array + ao_dn: jax.Array + paired_dn: jax.Array + paired_up_lambda: jax.Array + + +@jit +def _init_det_ratio_streaming_state( + geminal_data: "Geminal_data", + r_up_carts: jax.Array, + r_dn_carts: jax.Array, +) -> Det_ratio_streaming_state: + """Initialize the slim ratio streaming state at ``(r_up_carts, r_dn_carts)``. + + Performs one full AO evaluation per spin and two ``(n_ao, n_ao) @ (n_ao, N_e)`` + matmuls. Subsequent advances refresh only one column/row per accepted + single-electron move. + """ + # Storage zone: det_grad_lap (matches existing Det_streaming_state and + # is fp64 in both full/mixed modes). + dtype_jnp = get_dtype_jnp("det_grad_lap") + + lambda_matrix_paired, _lambda_matrix_unpaired = jnp.hsplit(geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + + # Forward r_up/dn_carts as-is (Principle 3a). compute_orb_api handles + # its own use-site casts. + ao_up = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_up_carts).astype(dtype_jnp) + ao_dn = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_dn_carts).astype(dtype_jnp) + + paired_dn = jnp.dot(lambda_matrix_paired, ao_dn) + paired_up_lambda = jnp.dot(ao_up.T, lambda_matrix_paired) + + return Det_ratio_streaming_state( + ao_up=ao_up, + ao_dn=ao_dn, + paired_dn=paired_dn, + paired_up_lambda=paired_up_lambda, + ) + + +@jit +def _advance_det_ratio_streaming_state( + geminal_data: "Geminal_data", + state: Det_ratio_streaming_state, + moved_spin_is_up: jax.Array, + moved_index: jax.Array, + r_up_carts_new: jax.Array, + r_dn_carts_new: jax.Array, +) -> Det_ratio_streaming_state: + """Advance the slim ratio streaming state after a single-electron move. + + The new ``(r_up_carts_new, r_dn_carts_new)`` differ from the configuration + represented by ``state`` in *exactly one* electron position, identified by + ``(moved_spin_is_up, moved_index)``. + + On up-move: refresh ``ao_up[:, k]`` and ``paired_up_lambda[k, :]``. + ``paired_dn`` is unchanged because it depends on ``ao_dn`` only. + On dn-move: refresh ``ao_dn[:, k]`` and ``paired_dn[:, k]``. + ``paired_up_lambda`` is unchanged because it depends on ``ao_up`` only. + + Cost: O(n_ao + n_ao^2) per call -- one single-electron AO eval plus one + ``(n_ao,) @ (n_ao, n_ao)`` matvec. + """ + dtype_jnp = get_dtype_jnp("det_grad_lap") + + lambda_matrix_paired, _lambda_matrix_unpaired = jnp.hsplit(geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn]) + lambda_matrix_paired = jnp.asarray(lambda_matrix_paired, dtype=dtype_jnp) + + def _branch_up(_): + # Single-electron AO eval at the new up position. + r_new = jnp.expand_dims(r_up_carts_new[moved_index], axis=0) # (1, 3) + ao_v = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_new) + ao_col = jnp.asarray(ao_v[:, 0], dtype=dtype_jnp) # (n_ao_up,) + + new_ao_up = state.ao_up.at[:, moved_index].set(ao_col) + # paired_up_lambda[k, :] = ao_col @ lambda_paired (n_ao_dn,) + new_paired_up_lambda_row = jnp.dot(ao_col, lambda_matrix_paired) + new_paired_up_lambda = state.paired_up_lambda.at[moved_index, :].set(new_paired_up_lambda_row) + # paired_dn unchanged (depends on ao_dn only). + return state.replace( + ao_up=new_ao_up, + paired_up_lambda=new_paired_up_lambda, + ) + + def _branch_dn(_): + # Single-electron AO eval at the new dn position. + r_new = jnp.expand_dims(r_dn_carts_new[moved_index], axis=0) + ao_v = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_new) + ao_col = jnp.asarray(ao_v[:, 0], dtype=dtype_jnp) # (n_ao_dn,) + + new_ao_dn = state.ao_dn.at[:, moved_index].set(ao_col) + # paired_dn[:, k] = lambda_paired @ ao_col (n_ao_up,) + new_paired_dn_col = jnp.dot(lambda_matrix_paired, ao_col) + new_paired_dn = state.paired_dn.at[:, moved_index].set(new_paired_dn_col) + # paired_up_lambda unchanged (depends on ao_up only). + return state.replace( + ao_dn=new_ao_dn, + paired_dn=new_paired_dn, + ) + + # Edge case: zero-electron spin sectors collapse the cond at trace time. + num_up = state.ao_up.shape[1] + num_dn = state.ao_dn.shape[1] + if num_up == 0: + return _branch_dn(None) + if num_dn == 0: + return _branch_up(None) + return jax.lax.cond(moved_spin_is_up, _branch_up, _branch_dn, operand=None) + + +@jit +def _compute_v_up_move_from_det_ratio_state( + geminal_data: "Geminal_data", + state: Det_ratio_streaming_state, + moved_index: jax.Array, + r_up_carts_proposed: jax.Array, +) -> jax.Array: + """Build ``v = (G_new[i, :] - G_old[i, :])`` for an up-move from cached state. + + Replaces the twice-called ``compute_geminal_up_one_row_elements`` pattern + in the MCMC walk body by reusing ``state.ao_up`` (old AO column) and + ``state.paired_dn`` (= ``lambda_paired @ ao_dn``). Avoids the heavy + ``lambda @ ao_dn`` GEMM and the N_dn-bulk AO evaluation that the old + pattern paid every step. + + Args: + geminal_data: Geminal parameters (used for orb_data_up_spin and + ``_lambda_matrix_jnp`` unpaired block). + state: Streaming state consistent with the current ``(r_up_carts, + r_dn_carts)``. + moved_index: Up-electron index that is being proposed to move. + r_up_carts_proposed: Proposed up-spin coordinates ``(N_up, 3)``; + differs from current ``r_up_carts`` in row ``moved_index`` only. + + Returns: + ``v`` with shape ``(N_up,)`` (= N_dn + num_unpaired). + """ + dtype_jnp = get_dtype_jnp("det_ratio") + + # Single-electron AO eval at the proposed position. + r_new = jnp.expand_dims(r_up_carts_proposed[moved_index], axis=0) # (1, 3) + ao_up_new_2d = geminal_data.compute_orb_api(geminal_data.orb_data_up_spin, r_new) + ao_up_new = jnp.asarray(ao_up_new_2d[:, 0], dtype=dtype_jnp) # (n_ao_up,) + + # Cached old AO column (use-site cast; Principle 3b). + ao_up_old = jnp.asarray(state.ao_up[:, moved_index], dtype=dtype_jnp) + delta_ao_up = ao_up_new - ao_up_old # (n_ao_up,) + + # Paired block: delta_ao_up @ paired_dn -> (N_dn,) + paired_dn = jnp.asarray(state.paired_dn, dtype=dtype_jnp) + v_paired = jnp.dot(delta_ao_up, paired_dn) + + # Unpaired block: delta_ao_up @ lambda_unpaired -> (num_unpaired,) + _lambda_matrix_paired, lambda_matrix_unpaired = jnp.hsplit(geminal_data._lambda_matrix_jnp, [geminal_data.orb_num_dn]) + lambda_matrix_unpaired = jnp.asarray(lambda_matrix_unpaired, dtype=dtype_jnp) + v_unpaired = jnp.dot(delta_ao_up, lambda_matrix_unpaired) + + return jnp.concatenate([v_paired, v_unpaired]) # (N_up,) + + +@jit +def _compute_u_dn_move_from_det_ratio_state( + geminal_data: "Geminal_data", + state: Det_ratio_streaming_state, + moved_index: jax.Array, + r_dn_carts_proposed: jax.Array, +) -> jax.Array: + """Build ``u = G_new[:, j] - G_old[:, j]`` for a dn-move from cached state. + + Replaces the twice-called ``compute_geminal_dn_one_column_elements`` + pattern in the MCMC walk body by reusing ``state.ao_dn`` (old AO column) + and ``state.paired_up_lambda`` (= ``ao_up.T @ lambda_paired``). + + Args: + geminal_data: Geminal parameters (used for orb_data_dn_spin). + state: Streaming state consistent with the current ``(r_up_carts, + r_dn_carts)``. + moved_index: Dn-electron index that is being proposed to move. + r_dn_carts_proposed: Proposed dn-spin coordinates ``(N_dn, 3)``; + differs from current ``r_dn_carts`` in row ``moved_index`` only. + + Returns: + ``u`` with shape ``(N_up,)``. + """ + dtype_jnp = get_dtype_jnp("det_ratio") + + r_new = jnp.expand_dims(r_dn_carts_proposed[moved_index], axis=0) + ao_dn_new_2d = geminal_data.compute_orb_api(geminal_data.orb_data_dn_spin, r_new) + ao_dn_new = jnp.asarray(ao_dn_new_2d[:, 0], dtype=dtype_jnp) # (n_ao_dn,) + + ao_dn_old = jnp.asarray(state.ao_dn[:, moved_index], dtype=dtype_jnp) + delta_ao_dn = ao_dn_new - ao_dn_old # (n_ao_dn,) + + # u = paired_up_lambda @ delta_ao_dn -> (N_up,) + paired_up_lambda = jnp.asarray(state.paired_up_lambda, dtype=dtype_jnp) + return jnp.dot(paired_up_lambda, delta_ao_dn) + + def _compute_grads_and_laplacian_ln_Det_fast_debug( geminal_data: Geminal_data, r_up_carts: jax.Array, diff --git a/jqmc/hamiltonians.py b/jqmc/hamiltonians.py index 27876be7..4483bb59 100644 --- a/jqmc/hamiltonians.py +++ b/jqmc/hamiltonians.py @@ -228,6 +228,7 @@ def compute_local_energy_fast( r_dn_carts: jnpt.ArrayLike, RT: jnpt.ArrayLike, geminal_inverse: jnpt.ArrayLike, + det_ratio_state=None, ) -> float: """Compute local energy using a precomputed geminal inverse. @@ -252,6 +253,10 @@ def compute_local_energy_fast( Precomputed inverse of the geminal matrix ``G(r_up_carts, r_dn_carts)`` with shape ``(N_up, N_up)``. Typically the Sherman-Morrison running inverse from the MCMC loop. + det_ratio_state: Optional :class:`Det_ratio_streaming_state` (or + superset) consistent with ``(r_up_carts, r_dn_carts)``. + Forwarded to ``compute_coulomb_potential_fast`` so the non-local + ECP ratio kernel can skip the AO/precontract recomputation. Returns: float: Local energy :math:`e_L` at the supplied configuration. @@ -284,6 +289,7 @@ def compute_local_energy_fast( RT=RT, A_old_inv=geminal_inverse, wavefunction_data=hamiltonian_data.wavefunction_data, + det_ratio_state=det_ratio_state, ) # Cast scalar zone outputs to local_energy zone at the sum (Principle 3b). diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index f42c9db6..171b0250 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -2560,14 +2560,17 @@ def _batch_pairwise_sum(points_a, points_b, param): # New position of the moved electron per config (select spin block) r_new_up_moved = jnp.take_along_axis(new_r_up_carts_arr, idx_up[:, None, None], axis=1).reshape(N_batch, 3) r_new_dn_moved = jnp.take_along_axis(new_r_dn_carts_arr, idx_dn[:, None, None], axis=1).reshape(N_batch, 3) - r_old_up_moved = old_r_up_carts[idx_up] # (N, 3) - r_old_dn_moved = old_r_dn_carts[idx_dn] # (N, 3) r_new_moved = jnp.where(up_moved_batch[:, None], r_new_up_moved, r_new_dn_moved) # (N, 3) - r_old_moved = jnp.where(up_moved_batch[:, None], r_old_up_moved, r_old_dn_moved) # (N, 3) - # Single batched AO evaluation for all N configs (replaces N per-config calls inside vmap) + # Single batched AO evaluation for all N configs (replaces N per-config calls inside vmap). + # Old AOs are obtained by column-slicing the already-computed + # ``aos_up_old`` / ``aos_dn_old`` (consistent with old_r_*_carts), avoiding + # a redundant ``compute_orb_api`` call on ``r_old_moved`` -- mirrors the + # gather trick already used in ``_compute_ratio_Jastrow_part_split_spin``. aos_new_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_new_moved), dtype=dtype_jnp) # (n_ao, N) - aos_old_batch = jnp.array(j3d.compute_orb_api(j3d.orb_data, r_old_moved), dtype=dtype_jnp) # (n_ao, N) + aos_up_old_at_idx = jnp.take(aos_up_old, idx_up, axis=1) # (n_ao, N) + aos_dn_old_at_idx = jnp.take(aos_dn_old, idx_dn, axis=1) # (n_ao, N) + aos_old_batch = jnp.where(up_moved_batch[None, :], aos_up_old_at_idx, aos_dn_old_at_idx) # (n_ao, N) aos_p_batch = aos_new_batch - aos_old_batch # (n_ao, N) # Precompute constant products (independent of config). With a @@ -4028,8 +4031,6 @@ def compute_grads_and_laplacian_Jastrow_three_body( # O(n_ao^2 * N_e + n_ao * N_e^2). Used by the GFMC projection inner loop # (jqmc_gfmc.py:_body_fun_n_streaming). # -# Design references: lrdmc_refactoring.md sections 1-1, 1-2, 1-4. -# # Lifetime: the state is freshly initialized at each branching boundary # (when _projection_n is re-entered) and advanced for at most # `num_mcmc_per_measurement` steps inside the fori_loop, mirroring the diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index d82dda4f..34911ed2 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -77,6 +77,8 @@ compute_ecp_non_local_parts_nearest_neighbors_fast_update, ) from .determinant import ( + _compute_u_dn_move_from_det_ratio_state, + _compute_v_up_move_from_det_ratio_state, compute_geminal_all_elements, compute_geminal_dn_one_column_elements, compute_geminal_up_one_row_elements, @@ -735,6 +737,7 @@ def _projection_t_core( non_local_move: bool, alat: float, hamiltonian_data: Hamiltonian_data, + det_ratio_state=None, ): """Single GFMC_t projection step, parameterized by per-electron continuum kinetic energy. @@ -785,6 +788,7 @@ def _projection_t_core( r_dn_carts=r_dn_carts, RT=R.T, j3_state=j3_state, + det_ratio_state=det_ratio_state, ) ) # spin-filp @@ -908,6 +912,7 @@ def _projection_t_core( A_old_inv=A_old_inv, RT=R.T, j3_state=j3_state, + det_ratio_state=det_ratio_state, ) ) @@ -927,6 +932,7 @@ def _projection_t_core( A_old_inv=A_old_inv, RT=R.T, j3_state=j3_state, + det_ratio_state=det_ratio_state, ) ) @@ -1031,18 +1037,29 @@ def _projection_t_core( dn_index = jnp.argmax(dn_diff) def _update_inv_up_t(_): - v = ( - compute_geminal_up_one_row_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_cart=jnp.reshape(new_r_up_carts[up_index], (1, 3)), - r_dn_carts=r_dn_carts, - ) - - compute_geminal_up_one_row_elements( + # Streaming path uses cached AO + paired_dn from + # ``det_ratio_state``; legacy path falls back to the + # twice-called row helper. + if det_ratio_state is None: + v = ( + compute_geminal_up_one_row_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_cart=jnp.reshape(new_r_up_carts[up_index], (1, 3)), + r_dn_carts=r_dn_carts, + ) + - compute_geminal_up_one_row_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_cart=jnp.reshape(r_up_carts[up_index], (1, 3)), + r_dn_carts=r_dn_carts, + ) + )[:, None] + else: + v = _compute_v_up_move_from_det_ratio_state( geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_cart=jnp.reshape(r_up_carts[up_index], (1, 3)), - r_dn_carts=r_dn_carts, - ) - )[:, None] + state=det_ratio_state, + moved_index=up_index, + r_up_carts_proposed=new_r_up_carts, + )[:, None] u = jax.nn.one_hot(up_index, num_up_electrons)[:, None] Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv @@ -1061,18 +1078,26 @@ def _no_update_t(_): else: def _update_inv_dn_t(_): - u = ( - compute_geminal_dn_one_column_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_carts=r_up_carts, - r_dn_cart=jnp.reshape(new_r_dn_carts[dn_index], (1, 3)), - ) - - compute_geminal_dn_one_column_elements( + if det_ratio_state is None: + u = ( + compute_geminal_dn_one_column_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_carts=r_up_carts, + r_dn_cart=jnp.reshape(new_r_dn_carts[dn_index], (1, 3)), + ) + - compute_geminal_dn_one_column_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_carts=r_up_carts, + r_dn_cart=jnp.reshape(r_dn_carts[dn_index], (1, 3)), + ) + )[:, None] + else: + u = _compute_u_dn_move_from_det_ratio_state( geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_carts=r_up_carts, - r_dn_cart=jnp.reshape(r_dn_carts[dn_index], (1, 3)), - ) - )[:, None] + state=det_ratio_state, + moved_index=dn_index, + r_dn_carts_proposed=new_r_dn_carts, + )[:, None] v = jax.nn.one_hot(dn_index, num_up_electrons)[:, None] Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv @@ -1196,6 +1221,7 @@ def _projection_t_streaming( non_local_move, alat, hamiltonian_data, + det_ratio_state=kinetic_state.det_state, ) moved_spin_is_up = has_up moved_index = jnp.where(has_up, up_idx, dn_idx) @@ -1211,10 +1237,12 @@ def _projection_t_streaming( return (e_L, pc, tl, wL, ru, rd, Ainv, kinetic_state_new, key, RT) # Python-static dispatch: streaming is incompatible with NN three-body - # Jastrow (J_NN has no rank-1 advance), and offers no benefit when J3 is - # absent. Mirrors the GFMC_n dispatch policy. + # Jastrow (J_NN has no rank-1 advance). The determinant streaming + # path (now consuming ``kinetic_state.det_state`` in the LRDMC mesh + # kernels and the GFMC inv-update) brings benefit even when J3 is + # absent, so the dispatch gate depends only on ``jastrow_nn_data``. jastrow_data = self.__hamiltonian_data.wavefunction_data.jastrow_data - use_streaming = jastrow_data.jastrow_nn_data is None and jastrow_data.jastrow_three_body_data is not None + use_streaming = jastrow_data.jastrow_nn_data is None # projection compilation. start_init = time.perf_counter() @@ -4780,6 +4808,7 @@ def _body_step_core( diagonal_kinetic_continuum_elements_up, diagonal_kinetic_continuum_elements_dn, j3_state=None, + det_ratio_state=None, ): """Single GFMC projection step, parameterized by per-electron continuum kinetic energy. @@ -4821,6 +4850,7 @@ def _body_step_core( r_dn_carts=r_dn_carts, RT=R.T, j3_state=j3_state, + det_ratio_state=det_ratio_state, ) ) # spin-filp @@ -4955,6 +4985,7 @@ def _body_step_core( A_old_inv=A_old_inv, RT=R.T, j3_state=j3_state, + det_ratio_state=det_ratio_state, ) ) @@ -4982,6 +5013,7 @@ def _body_step_core( A_old_inv=A_old_inv, RT=R.T, j3_state=j3_state, + det_ratio_state=det_ratio_state, ) ) @@ -5075,18 +5107,29 @@ def _body_step_core( dn_index = jnp.argmax(dn_diff) def _update_inv_up_n(_): - v = ( - compute_geminal_up_one_row_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_cart=jnp.reshape(proposed_r_up_carts[up_index], (1, 3)), - r_dn_carts=r_dn_carts, - ) - - compute_geminal_up_one_row_elements( + # v construction: streaming path uses cached AO + + # paired_dn from ``det_ratio_state``; legacy path falls + # back to the twice-called row helper. + if det_ratio_state is None: + v = ( + compute_geminal_up_one_row_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_cart=jnp.reshape(proposed_r_up_carts[up_index], (1, 3)), + r_dn_carts=r_dn_carts, + ) + - compute_geminal_up_one_row_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_cart=jnp.reshape(r_up_carts[up_index], (1, 3)), + r_dn_carts=r_dn_carts, + ) + )[:, None] + else: + v = _compute_v_up_move_from_det_ratio_state( geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_cart=jnp.reshape(r_up_carts[up_index], (1, 3)), - r_dn_carts=r_dn_carts, - ) - )[:, None] + state=det_ratio_state, + moved_index=up_index, + r_up_carts_proposed=proposed_r_up_carts, + )[:, None] u = jax.nn.one_hot(up_index, num_up_electrons)[:, None] Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv @@ -5105,18 +5148,26 @@ def _no_update_n(_): else: def _update_inv_dn_n(_): - u = ( - compute_geminal_dn_one_column_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_carts=r_up_carts, - r_dn_cart=jnp.reshape(proposed_r_dn_carts[dn_index], (1, 3)), - ) - - compute_geminal_dn_one_column_elements( + if det_ratio_state is None: + u = ( + compute_geminal_dn_one_column_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_carts=r_up_carts, + r_dn_cart=jnp.reshape(proposed_r_dn_carts[dn_index], (1, 3)), + ) + - compute_geminal_dn_one_column_elements( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + r_up_carts=r_up_carts, + r_dn_cart=jnp.reshape(r_dn_carts[dn_index], (1, 3)), + ) + )[:, None] + else: + u = _compute_u_dn_move_from_det_ratio_state( geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_carts=r_up_carts, - r_dn_cart=jnp.reshape(r_dn_carts[dn_index], (1, 3)), - ) - )[:, None] + state=det_ratio_state, + moved_index=dn_index, + r_dn_carts_proposed=proposed_r_dn_carts, + )[:, None] v = jax.nn.one_hot(dn_index, num_up_electrons)[:, None] Ainv_u = A_old_inv @ u vT_Ainv = v.T @ A_old_inv @@ -5243,6 +5294,7 @@ def _body_fun_n_streaming(i, carry): ke_up, ke_dn, j3_state=kinetic_state.j3_state, + det_ratio_state=kinetic_state.det_state, ) kinetic_state_new = _advance_kinetic_energy_all_elements_streaming_state( @@ -5276,14 +5328,14 @@ def _split_body(current_key, _): latest_jax_PRNG_key, (rotation_keys, move_keys) = _split_step_keys(init_jax_PRNG_key, num_mcmc_per_measurement) - # Python-static dispatch: the streaming path is incompatible with - # the NN three-body Jastrow (J_NN has no rank-1 advance -- see - # lrdmc_refactoring.md 1-4). When NN J3 is present, fall back to - # the legacy path that recomputes kinetic energies fresh each step. - # The streaming path is also compatible only when J3 is present; - # otherwise the gain over legacy is zero, so we still use legacy. + # Python-static dispatch: streaming is incompatible with NN three-body + # Jastrow (J_NN has no rank-1 advance). The determinant streaming + # path (now consuming ``kinetic_state.det_state`` in the LRDMC + # mesh kernels and the GFMC inv-update) brings benefit even when + # J3 is absent, so the dispatch gate depends only on + # ``jastrow_nn_data``. Mirrors the GFMC_t dispatch policy. jastrow_data = hamiltonian_data.wavefunction_data.jastrow_data - use_streaming = jastrow_data.jastrow_nn_data is None and jastrow_data.jastrow_three_body_data is not None + use_streaming = jastrow_data.jastrow_nn_data is None if use_streaming: init_kinetic_state = _init_kinetic_energy_all_elements_streaming_state( diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index 586a777d..97856b34 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -69,7 +69,12 @@ ) from .atomic_orbital import compute_overlap_matrix from .determinant import ( + Det_ratio_streaming_state, Geminal_data, + _advance_det_ratio_streaming_state, + _compute_u_dn_move_from_det_ratio_state, + _compute_v_up_move_from_det_ratio_state, + _init_det_ratio_streaming_state, compute_AS_regularization_factor, compute_AS_regularization_factor_fast_update, compute_det_geminal_all_elements, @@ -503,6 +508,15 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__latest_r_dn_carts, ) + # Warm-up slim state for the JIT trace. The real chain-entry init + # is rebuilt below in the run body; this throwaway value only exists + # to compile the update functions with the new argument shape. + det_ratio_state_warmup = _jit_vmap_init_det_ratio_state( + self.__hamiltonian_data.wavefunction_data.geminal_data, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + ) + dtype_jnp = jnp.float64 dtype_np = np.float64 @@ -525,6 +539,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__epsilon_AS, geminal_inv, geminal, + det_ratio_state_warmup, ) else: _ = _jit_vmap_update( @@ -537,9 +552,15 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__epsilon_AS, geminal_inv, geminal, + det_ratio_state_warmup, ) _ = _jit_vmap_e_L_fast( - self.__hamiltonian_data, self.__latest_r_up_carts, self.__latest_r_dn_carts, RTs, geminal_inv + self.__hamiltonian_data, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + RTs, + geminal_inv, + det_ratio_state_warmup, ) _ = _jit_vmap_as_reg( self.__hamiltonian_data.wavefunction_data.geminal_data, @@ -660,6 +681,17 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__latest_r_dn_carts, ) + # Slim AO + paired streaming state for ratio kernels. Built once at + # MCMC chain entry and advanced via + # ``_advance_det_ratio_streaming_state`` on every accepted move + # inside the fori_loop body. Lifecycle matches ``geminal_inv``: + # carried throughout the entire MCMC run, no periodic rebuild. + det_ratio_state = _jit_vmap_init_det_ratio_state( + self.__hamiltonian_data.wavefunction_data.geminal_data, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + ) + for i_mcmc_step in range(num_mcmc_steps): if (i_mcmc_step + 1) % mcmc_interval == 0: progress = (i_mcmc_step + self.__mcmc_counter + 1) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 @@ -679,6 +711,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__jax_PRNG_key_list, geminal_inv, geminal, + det_ratio_state, ) = _jit_vmap_update_up( self.__latest_r_up_carts, self.__latest_r_dn_carts, @@ -689,6 +722,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__epsilon_AS, geminal_inv, geminal, + det_ratio_state, ) else: ( @@ -699,6 +733,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__jax_PRNG_key_list, geminal_inv, geminal, + det_ratio_state, ) = _jit_vmap_update( self.__latest_r_up_carts, self.__latest_r_dn_carts, @@ -709,6 +744,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__epsilon_AS, geminal_inv, geminal, + det_ratio_state, ) self.__latest_r_up_carts.block_until_ready() self.__latest_r_dn_carts.block_until_ready() @@ -729,7 +765,12 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: start = time.perf_counter() # logger.debug(" Evaluating e_L ...") e_L_step = _jit_vmap_e_L_fast( - self.__hamiltonian_data, self.__latest_r_up_carts, self.__latest_r_dn_carts, RTs, geminal_inv + self.__hamiltonian_data, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + RTs, + geminal_inv, + det_ratio_state, ) e_L_step.block_until_ready() end = time.perf_counter() @@ -4283,6 +4324,7 @@ def _update_electron_positions( epsilon_AS, geminal_inv_init, geminal_init, + det_ratio_state_init, ): """Update electron positions based on the MH method. @@ -4294,6 +4336,13 @@ def _update_electron_positions( hamiltonian_data (Hamiltonian_data): an instance of Hamiltonian_data. Dt (float): the step size in the MH method. epsilon_AS (float): the exponent of the AS regularization. + geminal_inv_init: Sherman-Morrison running inverse for the geminal at + ``(init_r_up_carts, init_r_dn_carts)``. + geminal_init: Geminal matrix at ``(init_r_up_carts, init_r_dn_carts)``. + det_ratio_state_init: ``Det_ratio_streaming_state`` consistent with + ``(init_r_up_carts, init_r_dn_carts)``. Built once at the MCMC + chain entry by :func:`_init_det_ratio_streaming_state` and + advanced via :func:`_advance_det_ratio_streaming_state` on accept. Returns: jax_PRNG_key (jnpt.ArrayLike): updated jax_PRNG_key. @@ -4309,9 +4358,19 @@ def _update_electron_positions( r_dn_carts = jnp.asarray(init_r_dn_carts, dtype=dtype_jnp) geminal = geminal_init geminal_inv = geminal_inv_init + det_ratio_state = det_ratio_state_init def body_fun(_, carry): - accepted_moves, rejected_moves, r_up_carts, r_dn_carts, jax_PRNG_key, geminal_inv, geminal = carry + ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + jax_PRNG_key, + geminal_inv, + geminal, + det_ratio_state, + ) = carry total_electrons = len(r_up_carts) + len(r_dn_carts) num_up_electrons = len(r_up_carts) @@ -4407,25 +4466,20 @@ def body_fun(_, carry): )[0] # Determinant part, fast update using the matrix determinant lemma. - # Consumer-zone explicit cast: cast both lax.cond branches to the local - # mcmc zone dtype. The geminal-diff branch lives in the det_eval zone - # (fp64) while jax.nn.one_hot defaults to fp32, so without an explicit - # cast the cond branches disagree in mixed precision. + # The v / u construction reuses the cached AO + paired matrices in + # ``det_ratio_state`` so the bulk-side AO eval and ``lambda @ ao_dn`` + # GEMM are skipped. + # Consumer-zone explicit cast: cast both lax.cond branches to the + # local mcmc zone dtype so the branches agree under mixed precision + # (one_hot defaults to fp32, slim helpers return det_ratio zone). v = lax.cond( is_up, lambda _: jnp.asarray( - ( - compute_geminal_up_one_row_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - # inline "as_row3": force (1,3) even if source is (3,) - r_up_cart=jnp.reshape(proposed_r_up_carts[selected_electron_index], (1, 3)), - r_dn_carts=r_dn_carts, - ) - - compute_geminal_up_one_row_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_cart=jnp.reshape(r_up_carts[selected_electron_index], (1, 3)), - r_dn_carts=r_dn_carts, - ) + _compute_v_up_move_from_det_ratio_state( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + state=det_ratio_state, + moved_index=selected_electron_index, + r_up_carts_proposed=proposed_r_up_carts, )[:, None], dtype=dtype_jnp, ), @@ -4443,18 +4497,12 @@ def body_fun(_, carry): dtype=dtype_jnp, ), lambda _: jnp.asarray( - ( - compute_geminal_dn_one_column_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_carts=r_up_carts, - r_dn_cart=jnp.reshape(proposed_r_dn_carts[selected_electron_index], (1, 3)), # inline "as_row3" - ) - - compute_geminal_dn_one_column_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_carts=r_up_carts, - r_dn_cart=jnp.reshape(r_dn_carts[selected_electron_index], (1, 3)), - ) - )[:, None], # -> (N_up, 1) + _compute_u_dn_move_from_det_ratio_state( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + state=det_ratio_state, + moved_index=selected_electron_index, + r_dn_carts_proposed=proposed_r_dn_carts, + )[:, None], dtype=dtype_jnp, ), operand=None, @@ -4496,6 +4544,19 @@ def body_fun(_, carry): jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) b = jax.random.uniform(subkey, shape=(), minval=0.0, maxval=1.0) + # On accept, advance the slim ratio state to the new configuration. + # The advance refreshes one column of state.ao_{up,dn} and the + # corresponding row/column of state.paired_{up_lambda,dn} from a + # fresh single-electron AO eval (no rank-1 drift). + det_ratio_state_new = _advance_det_ratio_streaming_state( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + state=det_ratio_state, + moved_spin_is_up=is_up, + moved_index=selected_electron_index, + r_up_carts_new=proposed_r_up_carts, + r_dn_carts_new=proposed_r_dn_carts, + ) + def _accepted_fun(_): # Move accepted return ( @@ -4505,29 +4566,80 @@ def _accepted_fun(_): proposed_r_dn_carts, geminal_inv_new, geminal_new, + det_ratio_state_new, ) def _rejected_fun(_): # Move rejected - return (accepted_moves, rejected_moves + 1, r_up_carts, r_dn_carts, geminal_inv, geminal) + return ( + accepted_moves, + rejected_moves + 1, + r_up_carts, + r_dn_carts, + geminal_inv, + geminal, + det_ratio_state, + ) # judge accept or reject the propsed move using jax.lax.cond - accepted_moves, rejected_moves, r_up_carts, r_dn_carts, geminal_inv, geminal = lax.cond( - b < acceptance_ratio, _accepted_fun, _rejected_fun, operand=None + ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + geminal_inv, + geminal, + det_ratio_state, + ) = lax.cond(b < acceptance_ratio, _accepted_fun, _rejected_fun, operand=None) + + carry = ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + jax_PRNG_key, + geminal_inv, + geminal, + det_ratio_state, ) - - carry = (accepted_moves, rejected_moves, r_up_carts, r_dn_carts, jax_PRNG_key, geminal_inv, geminal) return carry # main MCMC loop - accepted_moves, rejected_moves, r_up_carts, r_dn_carts, jax_PRNG_key, geminal_inv, geminal = jax.lax.fori_loop( + ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + jax_PRNG_key, + geminal_inv, + geminal, + det_ratio_state, + ) = jax.lax.fori_loop( 0, num_mcmc_per_measurement, body_fun, - (accepted_moves, rejected_moves, r_up_carts, r_dn_carts, jax_PRNG_key, geminal_inv, geminal), + ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + jax_PRNG_key, + geminal_inv, + geminal, + det_ratio_state, + ), ) - return (accepted_moves, rejected_moves, r_up_carts, r_dn_carts, jax_PRNG_key, geminal_inv, geminal) + return ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + jax_PRNG_key, + geminal_inv, + geminal, + det_ratio_state, + ) @partial(jit, static_argnums=3) @@ -4541,8 +4653,14 @@ def _update_electron_positions_only_up_electron( epsilon_AS, geminal_inv_init, geminal_init, + det_ratio_state_init, ): - """Update electron positions based on the MH method (up-spin electrons only).""" + """Update electron positions based on the MH method (up-spin electrons only). + + See :func:`_update_electron_positions` for the slim state ``det_ratio_state_init`` + contract; here only up-electrons move so ``state.ao_dn`` and ``state.paired_dn`` + stay constant for the entire chain. + """ dtype_jnp = jnp.float64 accepted_moves = 0 rejected_moves = 0 @@ -4550,9 +4668,19 @@ def _update_electron_positions_only_up_electron( r_dn_carts = jnp.asarray(init_r_dn_carts, dtype=dtype_jnp) geminal_inv = geminal_inv_init geminal = geminal_init + det_ratio_state = det_ratio_state_init def body_fun(_, carry): - accepted_moves, rejected_moves, r_up_carts, r_dn_carts, jax_PRNG_key, geminal_inv, geminal = carry + ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + jax_PRNG_key, + geminal_inv, + geminal, + det_ratio_state, + ) = carry num_up_electrons = len(r_up_carts) # dummy jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) @@ -4634,19 +4762,15 @@ def body_fun(_, carry): Jastrow_T_p = jnp.asarray(Jastrow_T_p, dtype=dtype_jnp) Jastrow_T_o = jnp.asarray(Jastrow_T_o, dtype=dtype_jnp) - # Determinant part, fast update using the matrix determinant lemma - v = ( - compute_geminal_up_one_row_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - # inline "as_row3": force (1,3) even if source is (3,) - r_up_cart=jnp.reshape(proposed_r_up_carts[selected_electron_index], (1, 3)), - r_dn_carts=r_dn_carts, - ) - - compute_geminal_up_one_row_elements( - geminal_data=hamiltonian_data.wavefunction_data.geminal_data, - r_up_cart=jnp.reshape(r_up_carts[selected_electron_index], (1, 3)), - r_dn_carts=r_dn_carts, - ) + # Determinant part, fast update using the matrix determinant lemma. + # v is built from the cached AO + paired_dn -- skips the + # lambda @ ao_dn GEMM and the bulk-side AO eval paid by the legacy + # ``compute_geminal_up_one_row_elements`` pattern. + v = _compute_v_up_move_from_det_ratio_state( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + state=det_ratio_state, + moved_index=selected_electron_index, + r_up_carts_proposed=proposed_r_up_carts, )[:, None] u = jax.nn.one_hot(selected_electron_index, num_up_electrons)[:, None] @@ -4685,6 +4809,17 @@ def body_fun(_, carry): jax_PRNG_key, subkey = jax.random.split(jax_PRNG_key) b = jax.random.uniform(subkey, shape=(), minval=0.0, maxval=1.0) + # On accept, advance the slim ratio state (always up-move in this + # variant -- state.ao_dn / state.paired_dn stay constant). + det_ratio_state_new = _advance_det_ratio_streaming_state( + geminal_data=hamiltonian_data.wavefunction_data.geminal_data, + state=det_ratio_state, + moved_spin_is_up=jnp.bool_(True), + moved_index=selected_electron_index, + r_up_carts_new=proposed_r_up_carts, + r_dn_carts_new=proposed_r_dn_carts, + ) + def _accepted_fun(_): # Move accepted return ( @@ -4694,43 +4829,97 @@ def _accepted_fun(_): proposed_r_dn_carts, geminal_inv_new, geminal_new, + det_ratio_state_new, ) def _rejected_fun(_): # Move rejected - return (accepted_moves, rejected_moves + 1, r_up_carts, r_dn_carts, geminal_inv, geminal) + return ( + accepted_moves, + rejected_moves + 1, + r_up_carts, + r_dn_carts, + geminal_inv, + geminal, + det_ratio_state, + ) # judge accept or reject the propsed move using jax.lax.cond - accepted_moves, rejected_moves, r_up_carts, r_dn_carts, geminal_inv, geminal = lax.cond( - b < acceptance_ratio, _accepted_fun, _rejected_fun, operand=None + ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + geminal_inv, + geminal, + det_ratio_state, + ) = lax.cond(b < acceptance_ratio, _accepted_fun, _rejected_fun, operand=None) + + carry = ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + jax_PRNG_key, + geminal_inv, + geminal, + det_ratio_state, ) - - carry = (accepted_moves, rejected_moves, r_up_carts, r_dn_carts, jax_PRNG_key, geminal_inv, geminal) return carry # main MCMC loop - accepted_moves, rejected_moves, r_up_carts, r_dn_carts, jax_PRNG_key, geminal_inv, geminal = jax.lax.fori_loop( + ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + jax_PRNG_key, + geminal_inv, + geminal, + det_ratio_state, + ) = jax.lax.fori_loop( 0, num_mcmc_per_measurement, body_fun, - (accepted_moves, rejected_moves, r_up_carts, r_dn_carts, jax_PRNG_key, geminal_inv, geminal), + ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + jax_PRNG_key, + geminal_inv, + geminal, + det_ratio_state, + ), ) - return (accepted_moves, rejected_moves, r_up_carts, r_dn_carts, jax_PRNG_key, geminal_inv, geminal) + return ( + accepted_moves, + rejected_moves, + r_up_carts, + r_dn_carts, + jax_PRNG_key, + geminal_inv, + geminal, + det_ratio_state, + ) # Module-level vmap/jit wrappers for MCMC kernels. # Created once at import time so subsequent MCMC.run() calls reuse # the same Python function objects and hit JAX's compilation cache. _jit_vmap_update = jit( - vmap(_update_electron_positions, in_axes=(0, 0, 0, None, None, None, None, 0, 0)), + vmap(_update_electron_positions, in_axes=(0, 0, 0, None, None, None, None, 0, 0, 0)), static_argnums=3, ) _jit_vmap_update_up = jit( - vmap(_update_electron_positions_only_up_electron, in_axes=(0, 0, 0, None, None, None, None, 0, 0)), + vmap(_update_electron_positions_only_up_electron, in_axes=(0, 0, 0, None, None, None, None, 0, 0, 0)), static_argnums=3, ) -_jit_vmap_e_L_fast = jit(vmap(compute_local_energy_fast, in_axes=(None, 0, 0, 0, 0))) +_jit_vmap_init_det_ratio_state = jit( + vmap(_init_det_ratio_streaming_state, in_axes=(None, 0, 0)), +) +_jit_vmap_e_L_fast = jit(vmap(compute_local_energy_fast, in_axes=(None, 0, 0, 0, 0, 0))) _jit_vmap_as_reg = jit(vmap(compute_AS_regularization_factor, in_axes=(None, 0, 0))) _jit_vmap_generate_RTs = jit(vmap(_generate_rotation_matrix, in_axes=0)) _jit_vmap_as_reg_fast = jit(vmap(compute_AS_regularization_factor_fast_update, in_axes=(0, 0))) diff --git a/jqmc/wavefunction.py b/jqmc/wavefunction.py index a49da8dd..19810986 100644 --- a/jqmc/wavefunction.py +++ b/jqmc/wavefunction.py @@ -1773,6 +1773,7 @@ def compute_discretized_kinetic_energy_fast_update( r_dn_carts: jax.Array, RT: jax.Array, j3_state: "Jastrow_three_body_streaming_state | None" = None, + det_ratio_state=None, ) -> tuple[jax.Array, jax.Array, jax.Array]: r"""Fast-update version of discretized kinetic mesh and ratios. @@ -1794,6 +1795,12 @@ def compute_discretized_kinetic_energy_fast_update( recomputation. Use the value carried in the projection's ``Kinetic_streaming_state.j3_state``; pass ``None`` (default) for the original 1-shot path used by observation/MCMC code. + det_ratio_state: Optional :class:`Det_ratio_streaming_state` (or a + superset like :class:`Det_streaming_state`) consistent with + ``(r_up_carts, r_dn_carts)``. Forwarded to + ``_compute_ratio_determinant_part_split_spin`` so it can skip + the bulk-side AO eval and the two ``lambda_paired @ ao_*`` + precontracts. Returns: Tuple ``(r_up_carts_combined, r_dn_carts_combined, elements_kinetic_part)`` with combined @@ -1865,6 +1872,7 @@ def compute_discretized_kinetic_energy_fast_update( old_r_dn_carts=r_dn, new_r_up_shifted=r_up_carts_shifted, new_r_dn_shifted=r_dn_carts_shifted, + det_ratio_state=det_ratio_state, ), dtype=dtype_wf_ratio_jnp, ) diff --git a/tests/test_determinant.py b/tests/test_determinant.py index b8fed1ba..f9b8a705 100755 --- a/tests/test_determinant.py +++ b/tests/test_determinant.py @@ -50,6 +50,7 @@ from jqmc.atomic_orbital import AOs_sphe_data, compute_overlap_matrix from jqmc.determinant import ( Geminal_data, + _advance_det_ratio_streaming_state, _advance_grads_laplacian_ln_Det_streaming_state, _compute_AS_regularization_factor_debug, _compute_det_geminal_all_elements_debug, @@ -59,6 +60,8 @@ _compute_grads_and_laplacian_ln_Det_fast_debug, _compute_ratio_determinant_part_debug, _compute_ratio_determinant_part_rank1_update, + _compute_ratio_determinant_part_split_spin, + _init_det_ratio_streaming_state, _init_grads_laplacian_ln_Det_streaming_state, compute_AS_regularization_factor, compute_det_geminal_all_elements, @@ -1690,6 +1693,186 @@ def test_streaming_det_state_against_full(trexio_file: str): np.testing.assert_allclose(state.lap_ln_D_up, lap_up_ref, atol=atol, rtol=rtol) np.testing.assert_allclose(state.lap_ln_D_dn, lap_dn_ref, atol=atol, rtol=rtol) + # paired_up_lambda invariant (= ao_up.T @ lambda_paired). + state_ratio_ref = _init_det_ratio_streaming_state( + geminal_data=geminal_mo_data, + r_up_carts=r_up, + r_dn_carts=r_dn, + ) + np.testing.assert_allclose(state.paired_up_lambda, state_ratio_ref.paired_up_lambda, atol=atol, rtol=rtol) + + +# --------------------------------------------------------------------------- +# Det_ratio_streaming_state: slim ratio streaming state +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "trexio_file", + ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"], +) +def test_det_ratio_streaming_state_against_full(trexio_file: str): + """``Det_ratio_streaming_state`` after K random single-electron advances + must match a fresh ``_init_det_ratio_streaming_state`` at the resulting + configuration. + + Verifies the rank-1 advance invariants: + ao_up == compute_orb_api(orb_data_up_spin, r_up_carts) + ao_dn == compute_orb_api(orb_data_dn_spin, r_dn_carts) + paired_dn == lambda_paired @ ao_dn + paired_up_lambda == ao_up.T @ lambda_paired + """ + ( + _, + _, + _, + _, + geminal_mo_data, + _, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + + n_up = geminal_mo_data.num_electron_up + n_dn = geminal_mo_data.num_electron_dn + + rng = np.random.RandomState(13) + r_up = jnp.asarray(4.0 * rng.rand(n_up, 3) - 2.0) + r_dn = jnp.asarray(4.0 * rng.rand(n_dn, 3) - 2.0) + + state = _init_det_ratio_streaming_state( + geminal_data=geminal_mo_data, + r_up_carts=r_up, + r_dn_carts=r_dn, + ) + + K = 32 + for _ in range(K): + # alternate spin choices, but skip the spin if it has 0 electrons + if n_dn == 0: + spin_is_up = True + elif n_up == 0: + spin_is_up = False + else: + spin_is_up = bool(rng.randint(0, 2)) + + if spin_is_up: + idx = int(rng.randint(0, n_up)) + r_up = r_up.at[idx].set(jnp.asarray(rng.normal(size=(3,)) * 0.4 + np.asarray(r_up[idx]))) + else: + idx = int(rng.randint(0, n_dn)) + r_dn = r_dn.at[idx].set(jnp.asarray(rng.normal(size=(3,)) * 0.4 + np.asarray(r_dn[idx]))) + + state = _advance_det_ratio_streaming_state( + geminal_data=geminal_mo_data, + state=state, + moved_spin_is_up=jnp.bool_(spin_is_up), + moved_index=jnp.int32(idx), + r_up_carts_new=r_up, + r_dn_carts_new=r_dn, + ) + + # Reference: fresh init at the final configuration. + state_ref = _init_det_ratio_streaming_state( + geminal_data=geminal_mo_data, + r_up_carts=r_up, + r_dn_carts=r_dn, + ) + + atol, rtol = get_tolerance("det_grad_lap", "strict") + np.testing.assert_allclose(state.ao_up, state_ref.ao_up, atol=atol, rtol=rtol) + np.testing.assert_allclose(state.ao_dn, state_ref.ao_dn, atol=atol, rtol=rtol) + np.testing.assert_allclose(state.paired_dn, state_ref.paired_dn, atol=atol, rtol=rtol) + np.testing.assert_allclose(state.paired_up_lambda, state_ref.paired_up_lambda, atol=atol, rtol=rtol) + + +# --------------------------------------------------------------------------- +# _compute_ratio_determinant_part_split_spin with det_ratio_state plumb +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "trexio_file", + ["water_ccecp_ccpvqz.h5", "H2_ae_ccpvdz_cart.h5", "N_ae_ccpvdz_cart.h5"], +) +def test_split_spin_with_det_ratio_state_against_fresh(trexio_file: str): + """``_compute_ratio_determinant_part_split_spin`` must produce identical + determinant ratios when called with a consistent ``det_ratio_state`` vs + when called with ``det_ratio_state=None`` (legacy path). + + Builds a small block-structured mesh (one shifted up-config + one + shifted dn-config per direction) at a random reference configuration + and checks both ratio paths agree at strict tolerance. + """ + ( + _, + _, + _, + _, + geminal_mo_data, + _, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + + n_up = geminal_mo_data.num_electron_up + n_dn = geminal_mo_data.num_electron_dn + if n_up == 0 or n_dn == 0: + pytest.skip("split-spin only meaningful when both spin sectors are non-empty") + + rng = np.random.RandomState(7) + r_up = jnp.asarray(4.0 * rng.rand(n_up, 3) - 2.0) + r_dn = jnp.asarray(4.0 * rng.rand(n_dn, 3) - 2.0) + A_old_inv = _build_geminal_inverse(geminal_mo_data, r_up, r_dn) + + # Build a small block mesh: shift each up-electron by +0.1 in x; + # likewise for dn. (G_up = n_up, G_dn = n_dn.) + new_r_up_shifted = [] + for k in range(n_up): + shifted = np.array(r_up) + shifted[k] = shifted[k] + np.array([0.1, 0.0, 0.0]) + new_r_up_shifted.append(shifted) + new_r_up_shifted = jnp.asarray(np.stack(new_r_up_shifted, axis=0)) # (n_up, n_up, 3) + + new_r_dn_shifted = [] + for k in range(n_dn): + shifted = np.array(r_dn) + shifted[k] = shifted[k] + np.array([0.1, 0.0, 0.0]) + new_r_dn_shifted.append(shifted) + new_r_dn_shifted = jnp.asarray(np.stack(new_r_dn_shifted, axis=0)) # (n_dn, n_dn, 3) + + # Reference: legacy path. + ratios_ref = _compute_ratio_determinant_part_split_spin( + geminal_data=geminal_mo_data, + A_old_inv=A_old_inv, + old_r_up_carts=r_up, + old_r_dn_carts=r_dn, + new_r_up_shifted=new_r_up_shifted, + new_r_dn_shifted=new_r_dn_shifted, + det_ratio_state=None, + ) + + # Streaming path: build a fresh slim state at the reference config. + state = _init_det_ratio_streaming_state( + geminal_data=geminal_mo_data, + r_up_carts=r_up, + r_dn_carts=r_dn, + ) + ratios_streaming = _compute_ratio_determinant_part_split_spin( + geminal_data=geminal_mo_data, + A_old_inv=A_old_inv, + old_r_up_carts=r_up, + old_r_dn_carts=r_dn, + new_r_up_shifted=new_r_up_shifted, + new_r_dn_shifted=new_r_dn_shifted, + det_ratio_state=state, + ) + + atol, rtol = get_tolerance("det_ratio", "strict") + np.testing.assert_allclose(ratios_streaming, ratios_ref, atol=atol, rtol=rtol) + if __name__ == "__main__": from logging import Formatter, StreamHandler, getLogger diff --git a/tests/test_ecps.py b/tests/test_ecps.py index ad8f0906..d5c0939b 100755 --- a/tests/test_ecps.py +++ b/tests/test_ecps.py @@ -65,7 +65,7 @@ compute_ecp_non_local_parts_nearest_neighbors, compute_ecp_non_local_parts_nearest_neighbors_fast_update, ) -from jqmc.determinant import compute_geminal_all_elements +from jqmc.determinant import _init_det_ratio_streaming_state, compute_geminal_all_elements from jqmc.jastrow_factor import ( Jastrow_data, Jastrow_one_body_data, @@ -709,6 +709,98 @@ def test_fast_update_ecp_non_local_partial_NN(trexio_file: str): np.testing.assert_allclose(np.asarray(mesh_dn_fast), np.asarray(mesh_dn_ref), atol=atol, rtol=rtol) +@pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5"]) +def test_streaming_ecp_non_local_step_consistency(trexio_file: str): + """``compute_ecp_non_local_parts_nearest_neighbors_fast_update`` must + return identical mesh ratios when called with a consistent + ``det_ratio_state`` vs the legacy ``det_ratio_state=None`` path. + + The streaming caller forwards ``Det_ratio_streaming_state`` (or + ``Det_streaming_state``) so the kernel can skip the bulk-side AO eval + and the ``lambda @ ao_*`` precontracts. + """ + atol, rtol = get_tolerance("coulomb", "strict") + ( + structure_data, + _aos_data, + _mos_data_up, + _mos_data_dn, + geminal_mo_data, + coulomb_potential_data, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), + store_tuple=True, + ) + + rng = np.random.default_rng(0) + jastrow_data = _build_full_jastrow_data(structure_data, geminal_mo_data, coulomb_potential_data, rng) + wavefunction_data = Wavefunction_data(geminal_data=geminal_mo_data, jastrow_data=jastrow_data) + + r_up_carts = jnp.asarray(rng.uniform(-1.0, 1.0, size=(geminal_mo_data.num_electron_up, 3))) + r_dn_carts = jnp.asarray(rng.uniform(-1.0, 1.0, size=(geminal_mo_data.num_electron_dn, 3))) + + geminal_old = compute_geminal_all_elements( + geminal_data=geminal_mo_data, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, + ) + A_old_inv = jnp.linalg.inv(geminal_old) + + # Build a fresh slim state at the reference config (consistent with r_up/r_dn). + state = _init_det_ratio_streaming_state( + geminal_data=geminal_mo_data, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, + ) + + Nv = 6 + RT = jnp.eye(3) + + for NN in range(1, structure_data.natom): + # Legacy path (no state). + ( + mesh_up_legacy, + mesh_dn_legacy, + V_legacy, + sum_V_legacy, + ) = compute_ecp_non_local_parts_nearest_neighbors_fast_update( + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, + RT=RT, + A_old_inv=A_old_inv, + NN=NN, + Nv=Nv, + flag_determinant_only=False, + det_ratio_state=None, + ) + + # Streaming path (with state). + ( + mesh_up_streaming, + mesh_dn_streaming, + V_streaming, + sum_V_streaming, + ) = compute_ecp_non_local_parts_nearest_neighbors_fast_update( + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + r_up_carts=r_up_carts, + r_dn_carts=r_dn_carts, + RT=RT, + A_old_inv=A_old_inv, + NN=NN, + Nv=Nv, + flag_determinant_only=False, + det_ratio_state=state, + ) + + np.testing.assert_allclose(np.asarray(sum_V_streaming), np.asarray(sum_V_legacy), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(V_streaming), np.asarray(V_legacy), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(mesh_up_streaming), np.asarray(mesh_up_legacy), atol=atol, rtol=rtol) + np.testing.assert_allclose(np.asarray(mesh_dn_streaming), np.asarray(mesh_dn_legacy), atol=atol, rtol=rtol) + + @pytest.mark.parametrize("trexio_file", ["water_ccecp_ccpvqz.h5"]) def test_debug_and_jax_bare_el_ion_elements(trexio_file: str): """Test the bare couloumb potential computation.""" From 118f140d5ce55ce71b7b650418e5eab64cd381ef Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 7 May 2026 17:33:17 +0900 Subject: [PATCH 56/97] Improve J3 streaming cache in jastrow_factor.py --- jqmc/jastrow_factor.py | 50 +++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 171b0250..013ae684 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -2591,11 +2591,19 @@ def _batch_pairwise_sum(points_a, points_b, param): W_dn = j3_state.j3_mat_aos_dn.astype(dtype_jnp) U_up = j3_state.j3_mat_T_aos_up.astype(dtype_jnp).T U_dn = j3_state.j3_mat_T_aos_dn.astype(dtype_jnp).T - # cross_vec equivalences: - # j3_mat @ sum(aos_dn, axis=1) = sum(j3_mat @ aos_dn, axis=1) = sum(W_dn, axis=1) - # sum(aos_up, axis=1) @ j3_mat = sum(j3_mat.T @ aos_up, axis=1) = sum(j3_mat_T_aos_up, axis=1) - dn_cross_vec = jnp.sum(W_dn, axis=1) - up_cross_vec = jnp.sum(j3_state.j3_mat_T_aos_up.astype(dtype_jnp), axis=1) + # cross_vec are now precomputed and rank-1 advanced in the streaming + # state (``j3_mat_aos_dn_rowsum`` / ``j3_mat_T_aos_up_rowsum``). + # Equivalences: + # dn_cross_vec = j3_mat @ sum(aos_dn, axis=1) + # = sum(j3_mat @ aos_dn, axis=1) = sum(W_dn, axis=1) + # = j3_mat_aos_dn_rowsum + # up_cross_vec = sum(aos_up, axis=1) @ j3_mat + # = sum(j3_mat.T @ aos_up, axis=1) + # = j3_mat_T_aos_up_rowsum + # Reading the cached ``(n_ao,)`` rowsum avoids the per-call + # ``(W, n_ao, N_e)`` HBM-bound reduction. + dn_cross_vec = j3_state.j3_mat_aos_dn_rowsum.astype(dtype_jnp) + up_cross_vec = j3_state.j3_mat_T_aos_up_rowsum.astype(dtype_jnp) # Q index: idx_up for UP configs, idx_dn for DN configs idx_for_Q = jnp.where(up_moved_batch, idx_up, idx_dn) # (N,) @@ -2863,8 +2871,10 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: W_dn = j3_state.j3_mat_aos_dn.astype(dtype_jnp) U_up = j3_state.j3_mat_T_aos_up.astype(dtype_jnp).T U_dn = j3_state.j3_mat_T_aos_dn.astype(dtype_jnp).T - dn_cross_vec = jnp.sum(W_dn, axis=1) - up_cross_vec = jnp.sum(j3_state.j3_mat_T_aos_up.astype(dtype_jnp), axis=1) + # See ``_compute_ratio_Jastrow_part_rank1_update`` for why the + # cross_vec are now read from the cached rank-1-advanced rowsums. + dn_cross_vec = j3_state.j3_mat_aos_dn_rowsum.astype(dtype_jnp) + up_cross_vec = j3_state.j3_mat_T_aos_up_rowsum.astype(dtype_jnp) # -- UP BLOCK --------------------------------------------------------- # New AOs at the moved up-electron positions; old AOs by column-slice. @@ -2873,9 +2883,8 @@ def _pairwise_sums(pos1: jax.Array, pos2: jax.Array) -> jax.Array: aos_p_up = aos_up_new_moved - aos_up_old_moved # (n_ao, G_up) term1_up = j1_vec @ aos_p_up # (G_up,) - # tensordot avoids the explicit transpose of ``aos_p_up`` (1.8 GB on - # GH200 ECP-nonlocal benchmark) -- see ``_compute_ratio_Jastrow_part_rank1_update`` - # for the same rewrite rationale. + # tensordot avoids the explicit transpose of ``aos_p_up``; see + # ``_compute_ratio_Jastrow_part_rank1_update`` for the same rewrite rationale. V_up_block = jnp.tensordot(aos_p_up, W_up, axes=((0,), (0,))) # (G_up, N_up) P_up_block = jnp.dot(U_up, aos_p_up) # (N_up, G_up) Q_up_c = (idx_up_block[:, None] < jnp.arange(num_up)[None, :]).astype(dtype_jnp) # (G_up, N_up) @@ -4055,6 +4064,13 @@ class Jastrow_three_body_streaming_state: - ``lap_aos_up`` / ``lap_aos_dn``: ``(n_orb, N_up)`` / ``(n_orb, N_dn)``. - ``j3_mat_aos_up`` / ``j3_mat_aos_dn``: ``j3_mat @ aos_*`` (shapes match aos_*). - ``j3_mat_T_aos_up`` / ``j3_mat_T_aos_dn``: ``j3_mat.T @ aos_*``. + - ``j3_mat_aos_dn_rowsum`` / ``j3_mat_T_aos_up_rowsum``: ``(n_orb,)`` + row-sums ``sum(j3_mat_aos_dn, axis=1)`` / ``sum(j3_mat_T_aos_up, axis=1)``. + Equivalent to ``j3_mat @ sum(aos_dn, axis=1)`` and + ``sum(aos_up, axis=1) @ j3_mat`` and consumed by + :func:`_compute_ratio_Jastrow_part_rank1_update` as the + ``dn_cross_vec`` / ``up_cross_vec``. Caching avoids the + ``(W, n_orb, N_e)`` HBM-bound reduction every ratio call. - ``g_up`` / ``g_dn``: ``(n_orb, N_up)`` / ``(n_orb, N_dn)`` ``dJ/dA`` per electron. - ``grad_J3_up`` / ``grad_J3_dn``: ``(N_up, 3)`` / ``(N_dn, 3)`` per-electron grad. - ``lap_J3_up`` / ``lap_J3_dn``: ``(N_up,)`` / ``(N_dn,)`` per-electron lap. @@ -4070,6 +4086,8 @@ class Jastrow_three_body_streaming_state: j3_mat_aos_dn: jax.Array = struct.field(pytree_node=True) j3_mat_T_aos_up: jax.Array = struct.field(pytree_node=True) j3_mat_T_aos_dn: jax.Array = struct.field(pytree_node=True) + j3_mat_aos_dn_rowsum: jax.Array = struct.field(pytree_node=True) + j3_mat_T_aos_up_rowsum: jax.Array = struct.field(pytree_node=True) g_up: jax.Array = struct.field(pytree_node=True) g_dn: jax.Array = struct.field(pytree_node=True) grad_J3_up: jax.Array = struct.field(pytree_node=True) @@ -4144,6 +4162,10 @@ def _init_grads_laplacian_Jastrow_three_body_streaming_state( j3_mat_T_aos_up = j3_mat.T @ aos_up j3_mat_aos_dn = j3_mat @ aos_dn j3_mat_T_aos_dn = j3_mat.T @ aos_dn + # Row-sums consumed by ``_compute_ratio_Jastrow_part_rank1_update`` as + # ``dn_cross_vec`` / ``up_cross_vec`` (see field docstring above). + j3_mat_aos_dn_rowsum = jnp.sum(j3_mat_aos_dn, axis=1) + j3_mat_T_aos_up_rowsum = jnp.sum(j3_mat_T_aos_up, axis=1) upper_up = jnp.triu(jnp.ones((num_up, num_up), dtype=dtype_jnp), k=1) lower_up = jnp.tril(jnp.ones((num_up, num_up), dtype=dtype_jnp), k=-1) @@ -4179,6 +4201,8 @@ def _init_grads_laplacian_Jastrow_three_body_streaming_state( j3_mat_aos_dn=j3_mat_aos_dn, j3_mat_T_aos_up=j3_mat_T_aos_up, j3_mat_T_aos_dn=j3_mat_T_aos_dn, + j3_mat_aos_dn_rowsum=j3_mat_aos_dn_rowsum, + j3_mat_T_aos_up_rowsum=j3_mat_T_aos_up_rowsum, g_up=g_up, g_dn=g_dn, grad_J3_up=grad_J3_up, @@ -4271,6 +4295,9 @@ def _branch_up(_): lap_aos_up=new_lap_aos_up, j3_mat_aos_up=new_j3_mat_aos_up, j3_mat_T_aos_up=new_j3_mat_T_aos_up, + # Only j3_mat_T_aos_up changes (column ``moved_index``); the row-sum + # picks up exactly ``d_JT``. The dn row-sum is unchanged. + j3_mat_T_aos_up_rowsum=state.j3_mat_T_aos_up_rowsum + d_JT, g_up=new_g_up, g_dn=new_g_dn, grad_J3_up=grad_J3_up, @@ -4317,6 +4344,9 @@ def _branch_dn(_): lap_aos_dn=new_lap_aos_dn, j3_mat_aos_dn=new_j3_mat_aos_dn, j3_mat_T_aos_dn=new_j3_mat_T_aos_dn, + # Only j3_mat_aos_dn changes (column ``moved_index``); the row-sum + # picks up exactly ``d_J``. The up row-sum is unchanged. + j3_mat_aos_dn_rowsum=state.j3_mat_aos_dn_rowsum + d_J, g_up=new_g_up, g_dn=new_g_dn, grad_J3_up=grad_J3_up, From c11dc9d72fece5a63544b52b7548c9802ed548cd Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 7 May 2026 18:15:49 +0900 Subject: [PATCH 57/97] Improve loggers in jqmc/jqmc_gfmc.py and jqmc/jqmc_mcmc.py. --- jqmc/jqmc_gfmc.py | 59 +++++++++++++++++++++++++---------------------- jqmc/jqmc_mcmc.py | 31 ++++++++++++++----------- 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 34911ed2..18da7468 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -1682,12 +1682,6 @@ def _run_projection_loop_streaming(pcl, tll, wll, ru, rd, Ainv, key, ks): logger.info("") logger.info("-Start branching-") - progress = (self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 - gmfc_total_current = time.perf_counter() - logger.info( - f" branching step = {self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %. Elapsed time = {(gmfc_total_current - gfmc_total_start):.1f} sec." - ) - num_mcmc_done = 0 # -- Extend stored arrays with zero-padding for new steps -- @@ -1717,12 +1711,13 @@ def _run_projection_loop_streaming(pcl, tll, wll, ru, rd, Ainv, key, ks): [self.__stored_E_L_force_PP, np.zeros((num_mcmc_steps, 1, n_atoms, 3), dtype=dtype_np)] ) + gfmc_loop_start = time.perf_counter() for i_branching in range(num_mcmc_steps): - if (i_branching + 1) % gfmc_interval == 0: - progress = (i_branching + self.__mcmc_counter + 1) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + if i_branching % gfmc_interval == 0: + progress = (i_branching + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 gmfc_total_current = time.perf_counter() logger.info( - f" branching step = {i_branching + self.__mcmc_counter + 1}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %. Elapsed time = {(gmfc_total_current - gfmc_total_start):.1f} sec." + f" branching step = {i_branching + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %. Elapsed time = {(gmfc_total_current - gfmc_loop_start):.1f} sec." ) # Always set the initial weight list to 1.0 @@ -2340,6 +2335,11 @@ def _run_projection_loop_streaming(pcl, tll, wll, ru, rd, Ainv, key, ks): # count up, here is the end of the branching step. num_mcmc_done += 1 + progress = (num_mcmc_done + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + gmfc_total_current = time.perf_counter() + logger.info( + f" branching step = {num_mcmc_done + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %. Elapsed time = {(gmfc_total_current - gfmc_loop_start):.1f} sec." + ) logger.info("") # count up @@ -3355,15 +3355,13 @@ def _compute_local_energy_t_debug( gfmc_interval = int(np.maximum(num_mcmc_steps / 100, 1)) # gfmc_projection set print-interval logger.info("-Start branching-") - progress = (self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 - logger.info(f" branching step = {self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %.") num_mcmc_done = 0 for i_branching in range(num_mcmc_steps): - if (i_branching + 1) % gfmc_interval == 0: - progress = (i_branching + self.__mcmc_counter + 1) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + if i_branching % gfmc_interval == 0: + progress = (i_branching + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 logger.info( - f" branching step = {i_branching + self.__mcmc_counter + 1}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %." + f" branching step = {i_branching + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %." ) # Always set the initial weight list to 1.0 @@ -3981,6 +3979,10 @@ def _compute_local_energy_t_debug( # count up, here is the end of the branching step. num_mcmc_done += 1 + progress = (num_mcmc_done + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + logger.info( + f" branching step = {num_mcmc_done + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %." + ) logger.info("") # count up mcmc_counter @@ -5810,19 +5812,15 @@ def _compute_local_energy_n( [self.__stored_E_L_force_PP, np.zeros((num_mcmc_steps, 1, n_atoms, 3), dtype=dtype_np)] ) - progress = (self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 - gfmc_total_current = time.perf_counter() - logger.info( - f" Progress: GFMC step = {self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.0f} %. Elapsed time = {(gfmc_total_current - gfmc_total_start):.1f} sec." - ) mcmc_interval = int(np.maximum(num_mcmc_steps / 100, 1)) + gfmc_loop_start = time.perf_counter() for i_mcmc_step in range(num_mcmc_steps): - if (i_mcmc_step + 1) % mcmc_interval == 0: - progress = (i_mcmc_step + self.__mcmc_counter + 1) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + if i_mcmc_step % mcmc_interval == 0: + progress = (i_mcmc_step + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 gfmc_total_current = time.perf_counter() logger.info( - f" Progress: GFMC step = {i_mcmc_step + self.__mcmc_counter + 1}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %. Elapsed time = {(gfmc_total_current - gfmc_total_start):.1f} sec." + f" Progress: GFMC step = {i_mcmc_step + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %. Elapsed time = {(gfmc_total_current - gfmc_loop_start):.1f} sec." ) # Always set the initial weight list to 1.0 @@ -6462,6 +6460,11 @@ def _compute_local_energy_n( # count up, here is the end of the branching step. num_mcmc_done += 1 + progress = (num_mcmc_done + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + gfmc_total_current = time.perf_counter() + logger.info( + f" Progress: GFMC step = {num_mcmc_done + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %. Elapsed time = {(gfmc_total_current - gfmc_loop_start):.1f} sec." + ) logger.info("") # count up mcmc_counter @@ -7832,16 +7835,14 @@ def _compute_local_energy_n_debug( # MAIN MCMC loop from here !!! logger.info("Start GFMC") num_mcmc_done = 0 - progress = (self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 - logger.info(f" Progress: GFMC step = {self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.0f} %.") mcmc_interval = int(np.maximum(num_mcmc_steps / 100, 1)) for i_mcmc_step in range(num_mcmc_steps): - if (i_mcmc_step + 1) % mcmc_interval == 0: - progress = (i_mcmc_step + self.__mcmc_counter + 1) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + if i_mcmc_step % mcmc_interval == 0: + progress = (i_mcmc_step + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 logger.info( - f" Progress: GFMC step = {i_mcmc_step + self.__mcmc_counter + 1}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %." + f" Progress: GFMC step = {i_mcmc_step + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %." ) # Always set the initial weight list to 1.0 @@ -8236,6 +8237,10 @@ def _compute_local_energy_n_debug( # count up, here is the end of the branching step. num_mcmc_done += 1 + progress = (num_mcmc_done + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + logger.info( + f" Progress: GFMC step = {num_mcmc_done + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %." + ) logger.info("") # count up mcmc_counter diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index 97856b34..e0eca0f3 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -648,11 +648,6 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: # MAIN MCMC loop from here !!! logger.info("Start MCMC") num_mcmc_done = 0 - progress = (self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 - mcmc_total_current = time.perf_counter() - logger.info( - f" Progress: MCMC step= {self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.0f} %. Elapsed time = {(mcmc_total_current - mcmc_total_start):.1f} sec." - ) mcmc_interval = max(1, int(num_mcmc_steps / 100)) # % # adjust_epsilon_AS = self.__adjust_epsilon_AS @@ -692,12 +687,13 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__latest_r_dn_carts, ) + mcmc_loop_start = time.perf_counter() for i_mcmc_step in range(num_mcmc_steps): - if (i_mcmc_step + 1) % mcmc_interval == 0: - progress = (i_mcmc_step + self.__mcmc_counter + 1) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + if i_mcmc_step % mcmc_interval == 0: + progress = (i_mcmc_step + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 mcmc_total_current = time.perf_counter() logger.info( - f" Progress: MCMC step = {i_mcmc_step + self.__mcmc_counter + 1}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %. Elapsed time = {(mcmc_total_current - mcmc_total_start):.1f} sec." + f" Progress: MCMC step = {i_mcmc_step + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %. Elapsed time = {(mcmc_total_current - mcmc_loop_start):.1f} sec." ) # electron positions are goint to be updated! @@ -1006,6 +1002,12 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: logger.info(" Break the mcmc loop.") break + progress = (num_mcmc_done + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + mcmc_total_current = time.perf_counter() + logger.info( + f" Progress: MCMC step = {num_mcmc_done + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %. Elapsed time = {(mcmc_total_current - mcmc_loop_start):.1f} sec." + ) + # Barrier after MCMC operation start = time.perf_counter() mpi_comm.Barrier() @@ -5122,15 +5124,13 @@ def run(self, num_mcmc_steps: int = 0) -> None: # MAIN MCMC loop from here !!! logger.info("Start MCMC") num_mcmc_done = 0 - progress = (self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 - logger.info(f" Progress: MCMC step= {self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.0f} %.") mcmc_interval = max(1, int(num_mcmc_steps / 10)) # % for i_mcmc_step in range(num_mcmc_steps): - if (i_mcmc_step + 1) % mcmc_interval == 0: - progress = (i_mcmc_step + self.__mcmc_counter + 1) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + if i_mcmc_step % mcmc_interval == 0: + progress = (i_mcmc_step + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 logger.info( - f" Progress: MCMC step = {i_mcmc_step + self.__mcmc_counter + 1}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %" + f" Progress: MCMC step = {i_mcmc_step + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %" ) accepted_moves_nw = np.zeros(self.__num_walkers, dtype=np.int32) @@ -5485,6 +5485,11 @@ def run(self, num_mcmc_steps: int = 0) -> None: num_mcmc_done += 1 + progress = (num_mcmc_done + self.__mcmc_counter) / (num_mcmc_steps + self.__mcmc_counter) * 100.0 + logger.info( + f" Progress: MCMC step = {num_mcmc_done + self.__mcmc_counter}/{num_mcmc_steps + self.__mcmc_counter}: {progress:.1f} %" + ) + logger.info("End MCMC") logger.info("") From 60f8a82b647b7d5669d962de663d58fcf4321e19 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 7 May 2026 22:14:13 +0900 Subject: [PATCH 58/97] Improved the K state carry in jqmc/wavefunction.py. --- jqmc/wavefunction.py | 121 +++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 57 deletions(-) diff --git a/jqmc/wavefunction.py b/jqmc/wavefunction.py index 19810986..5bb77c07 100644 --- a/jqmc/wavefunction.py +++ b/jqmc/wavefunction.py @@ -1269,30 +1269,25 @@ def _compute_kinetic_energy_all_elements_fast_update_debug( class Kinetic_streaming_state: """Streaming state for per-electron kinetic-energy evaluation. - Fields evaluated at the current ``(r_up_carts, r_dn_carts)``: - - - ``j3_state``: J3 auxiliary tables (None if no J3 component is active). - - ``det_state``: det auxiliary tables (always populated in PR2+; the - determinant per-electron grad/lap fields below mirror its outputs). - - ``grad_J_up`` / ``grad_J_dn``: total Jastrow per-electron gradient. - - ``lap_J_up`` / ``lap_J_dn``: total Jastrow per-electron Laplacian. - - ``grad_ln_D_up`` / ``grad_ln_D_dn``: per-electron ``nablaln|Det|`` from the - geminal at the current ``A_old_inv``. - - ``lap_ln_D_up`` / ``lap_ln_D_dn``: per-electron ``nabla^2ln|Det|``. + Carry-fields at the current ``(r_up_carts, r_dn_carts)``: + + - ``j1_state`` / ``j2_state`` / ``j3_state``: per-Jastrow streaming + sub-states (None when the corresponding component is absent). + - ``det_state``: det streaming sub-state (always populated in PR2+). + - ``ke_up`` / ``ke_dn``: per-electron kinetic-energy values, pre-assembled + so that :func:`_kinetic_energy_from_streaming_state` becomes a free + struct-field read. Both init and advance compute these from the + freshly produced grad/lap totals while those are still register-resident, + avoiding a separate read kernel that would otherwise re-load all + grad/lap totals from DRAM. """ j1_state: Jastrow_one_body_streaming_state | None = struct.field(pytree_node=True, default=None) j2_state: Jastrow_two_body_streaming_state | None = struct.field(pytree_node=True, default=None) j3_state: Jastrow_three_body_streaming_state | None = struct.field(pytree_node=True, default=None) det_state: Det_streaming_state | None = struct.field(pytree_node=True, default=None) - grad_J_up: jax.Array = struct.field(pytree_node=True, default=None) - grad_J_dn: jax.Array = struct.field(pytree_node=True, default=None) - lap_J_up: jax.Array = struct.field(pytree_node=True, default=None) - lap_J_dn: jax.Array = struct.field(pytree_node=True, default=None) - grad_ln_D_up: jax.Array = struct.field(pytree_node=True, default=None) - grad_ln_D_dn: jax.Array = struct.field(pytree_node=True, default=None) - lap_ln_D_up: jax.Array = struct.field(pytree_node=True, default=None) - lap_ln_D_dn: jax.Array = struct.field(pytree_node=True, default=None) + ke_up: jax.Array = struct.field(pytree_node=True, default=None) + ke_dn: jax.Array = struct.field(pytree_node=True, default=None) def _kinetic_energy_from_grads_laps( @@ -1383,34 +1378,38 @@ def _init_kinetic_energy_all_elements_streaming_state( else None ) + # Assemble per-electron kinetic energies once here so the streaming carry + # only stores the small ke_up/ke_dn arrays. Subsequent reads via + # _kinetic_energy_from_streaming_state become free struct-field accesses. + ke_up, ke_dn = _kinetic_energy_from_grads_laps( + grad_J_up, + grad_J_dn, + lap_J_up, + lap_J_dn, + det_state.grad_ln_D_up, + det_state.grad_ln_D_dn, + det_state.lap_ln_D_up, + det_state.lap_ln_D_dn, + ) + return Kinetic_streaming_state( j1_state=j1_state, j2_state=j2_state, j3_state=j3_state, det_state=det_state, - grad_J_up=grad_J_up, - grad_J_dn=grad_J_dn, - lap_J_up=lap_J_up, - lap_J_dn=lap_J_dn, - grad_ln_D_up=det_state.grad_ln_D_up, - grad_ln_D_dn=det_state.grad_ln_D_dn, - lap_ln_D_up=det_state.lap_ln_D_up, - lap_ln_D_dn=det_state.lap_ln_D_dn, + ke_up=ke_up, + ke_dn=ke_dn, ) def _kinetic_energy_from_streaming_state(state: Kinetic_streaming_state): - """Per-electron kinetic energies extracted from a streaming state.""" - return _kinetic_energy_from_grads_laps( - state.grad_J_up, - state.grad_J_dn, - state.lap_J_up, - state.lap_J_dn, - state.grad_ln_D_up, - state.grad_ln_D_dn, - state.lap_ln_D_up, - state.lap_ln_D_dn, - ) + """Per-electron kinetic energies extracted from a streaming state. + + Returns the pre-assembled ``ke_up`` / ``ke_dn`` carried in the streaming + state. Init and advance produce these in the same kernel that produced + the grad/lap totals, so this read costs no DRAM traffic of its own. + """ + return state.ke_up, state.ke_dn def _advance_kinetic_energy_all_elements_streaming_state( @@ -1454,10 +1453,10 @@ def _advance_kinetic_energy_all_elements_streaming_state( lap_J1_dn = new_j1_state.lap_J1_dn else: new_j1_state = None - grad_J1_up = jnp.zeros_like(state.grad_J_up) - grad_J1_dn = jnp.zeros_like(state.grad_J_dn) - lap_J1_up = jnp.zeros_like(state.lap_J_up) - lap_J1_dn = jnp.zeros_like(state.lap_J_dn) + grad_J1_up = jnp.zeros_like(state.det_state.grad_ln_D_up) + grad_J1_dn = jnp.zeros_like(state.det_state.grad_ln_D_dn) + lap_J1_up = jnp.zeros_like(state.det_state.lap_ln_D_up) + lap_J1_dn = jnp.zeros_like(state.det_state.lap_ln_D_dn) # --- J2: incremental advance via streaming state --------------------- j2_data = jastrow_data.jastrow_two_body_data @@ -1476,10 +1475,10 @@ def _advance_kinetic_energy_all_elements_streaming_state( lap_J2_dn = new_j2_state.lap_J2_dn else: new_j2_state = None - grad_J2_up = jnp.zeros_like(state.grad_J_up) - grad_J2_dn = jnp.zeros_like(state.grad_J_dn) - lap_J2_up = jnp.zeros_like(state.lap_J_up) - lap_J2_dn = jnp.zeros_like(state.lap_J_dn) + grad_J2_up = jnp.zeros_like(state.det_state.grad_ln_D_up) + grad_J2_dn = jnp.zeros_like(state.det_state.grad_ln_D_dn) + lap_J2_up = jnp.zeros_like(state.det_state.lap_ln_D_up) + lap_J2_dn = jnp.zeros_like(state.det_state.lap_ln_D_dn) # --- J3: incremental advance via streaming state --------------------- j3_data = jastrow_data.jastrow_three_body_data @@ -1498,10 +1497,10 @@ def _advance_kinetic_energy_all_elements_streaming_state( lap_J3_dn = new_j3_state.lap_J3_dn else: new_j3_state = None - grad_J3_up = jnp.zeros_like(state.grad_J_up) - grad_J3_dn = jnp.zeros_like(state.grad_J_dn) - lap_J3_up = jnp.zeros_like(state.lap_J_up) - lap_J3_dn = jnp.zeros_like(state.lap_J_dn) + grad_J3_up = jnp.zeros_like(state.det_state.grad_ln_D_up) + grad_J3_dn = jnp.zeros_like(state.det_state.grad_ln_D_dn) + lap_J3_up = jnp.zeros_like(state.det_state.lap_ln_D_up) + lap_J3_dn = jnp.zeros_like(state.det_state.lap_ln_D_dn) # Reassemble Jastrow totals from the streamed sub-state contributions. grad_J_up = grad_J1_up + grad_J2_up + grad_J3_up @@ -1540,19 +1539,27 @@ def _advance_kinetic_energy_all_elements_streaming_state( lap_J_up = jnp.asarray(lap_J_up, dtype=dtype_jnp) lap_J_dn = jnp.asarray(lap_J_dn, dtype=dtype_jnp) + # Assemble per-electron kinetic energies in-kernel: the new grad/lap + # totals are still register-resident here, so this avoids the dedicated + # read kernel that would otherwise re-load all 8 arrays from DRAM. + ke_up, ke_dn = _kinetic_energy_from_grads_laps( + grad_J_up, + grad_J_dn, + lap_J_up, + lap_J_dn, + new_det_state.grad_ln_D_up, + new_det_state.grad_ln_D_dn, + new_det_state.lap_ln_D_up, + new_det_state.lap_ln_D_dn, + ) + return state.replace( j1_state=new_j1_state, j2_state=new_j2_state, j3_state=new_j3_state, det_state=new_det_state, - grad_J_up=grad_J_up, - grad_J_dn=grad_J_dn, - lap_J_up=lap_J_up, - lap_J_dn=lap_J_dn, - grad_ln_D_up=new_det_state.grad_ln_D_up, - grad_ln_D_dn=new_det_state.grad_ln_D_dn, - lap_ln_D_up=new_det_state.lap_ln_D_up, - lap_ln_D_dn=new_det_state.lap_ln_D_dn, + ke_up=ke_up, + ke_dn=ke_dn, ) From 27c49f72f8072725d8130286810811b468b73f9b Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Fri, 8 May 2026 14:21:59 +0900 Subject: [PATCH 59/97] Implemenet prototype on-GPU optimization. --- jqmc/jqmc_mcmc.py | 887 ++++++++++++++++++++++++++++++---------- tests/test_jqmc_mcmc.py | 238 +++++++++++ 2 files changed, 912 insertions(+), 213 deletions(-) diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index e0eca0f3..f1574085 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -121,6 +121,222 @@ def _loglevel_devel(self, message, *args, **kwargs): mpi_size = mpi_comm.Get_size() +# --------------------------------------------------------------------------- +# Device-resident SR solvers (JAX-native, sharding + psum / all_gather) +# +# Implements the four SR solve paths from ``optimizer_on_gpu.md``: +# - wide + direct : (X X^T + eps I) y = X F via psum +# - wide + CG : same system, conjugate gradient with psum'd matvec +# - tall + direct : (X^T X + eps I) z = F, theta = X z via all_gather +# - tall + CG : same system, conjugate gradient on replicated inputs +# +# Compiled kernels are cached at module level so the JIT cost is paid once +# per process. The same code path runs on: +# - single-process CPU (psum is trivial; mesh has 1 device) +# - multi-process CPU + Gloo (after ``jax.distributed.initialize``) +# - GPU (single or multi process; psum maps to NVLink / NCCL) +# --------------------------------------------------------------------------- +def _get_sr_mesh(): + """Lazy-built 1-D ``Mesh`` over all visible JAX devices, axis name 'rank'.""" + cached = getattr(_get_sr_mesh, "_cached", None) + if cached is not None: + return cached + from jax.sharding import Mesh + + mesh = Mesh(np.array(jax.devices()), axis_names=("rank",)) + _get_sr_mesh._cached = mesh + return mesh + + +def _cg_while_loop(b, apply_A, x0, max_iter, tol, dtype): + """Plain CG with break-on-breakdown, packed into ``jax.lax.while_loop``. + + Returns ``(x, sqrt(rs), num_iter)`` matching the NumPy reference's + return signature. ``apply_A`` may close over ``psum`` collectives so + this helper is only safe to call from inside a ``shard_map``. + """ + tiny = jnp.asarray(jnp.finfo(dtype).tiny, dtype=dtype) + tol_sq = tol * tol + + r0 = b - apply_A(x0) + rs0 = jnp.dot(r0, r0) + state0 = ( + x0, + r0, + r0, # p + rs0, + jnp.int32(0), + jnp.bool_(False), # breakdown + ) + + def cond(state): + _x, _r, _p, rs, k, breakdown = state + return (k < max_iter) & (rs > tol_sq) & jnp.logical_not(breakdown) + + def body(state): + x, r, p, rs_old, k, breakdown = state + Ap = apply_A(p) + denom = jnp.dot(p, Ap) + new_breakdown = breakdown | jnp.logical_not(jnp.isfinite(denom)) | (jnp.abs(denom) <= tiny) + safe_denom = jnp.where(new_breakdown, jnp.asarray(1.0, dtype=dtype), denom) + alpha = rs_old / safe_denom + x_new = jnp.where(new_breakdown, x, x + alpha * p) + r_new = jnp.where(new_breakdown, r, r - alpha * Ap) + rs_new_real = jnp.dot(r_new, r_new) + rs_new = jnp.where(new_breakdown, rs_old, rs_new_real) + safe_rs_old = jnp.where(rs_old > 0, rs_old, jnp.asarray(1.0, dtype=dtype)) + beta = rs_new / safe_rs_old + p_new = jnp.where(new_breakdown, p, r_new + beta * p) + return (x_new, r_new, p_new, rs_new, k + 1, new_breakdown) + + x_f, _r_f, _p_f, rs_f, k_f, _bk_f = jax.lax.while_loop(cond, body, state0) + return x_f, jnp.sqrt(rs_f), k_f + + +def _get_sr_wide_direct_kernel(): + """Wide-matrix direct SR solve: ``theta = (X X^T + eps I)^{-1} (X F)``. + + Inputs sharded along sample axis 'rank'. Output replicated on every rank. + """ + cached = getattr(_get_sr_wide_direct_kernel, "_cached", None) + if cached is not None: + return cached + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + + @partial( + jax.shard_map, + mesh=mesh, + in_specs=(PSpec(None, "rank"), PSpec("rank"), PSpec()), + out_specs=PSpec(), + ) + def _solve(X, F, epsilon): + XXT_local = X @ X.T + XF_local = X @ F + XXT = jax.lax.psum(XXT_local, "rank") + XF = jax.lax.psum(XF_local, "rank") + XXT = XXT + epsilon * jnp.eye(XXT.shape[0], dtype=XXT.dtype) + return jnp.linalg.solve(XXT, XF) + + _get_sr_wide_direct_kernel._cached = _solve + return _solve + + +def _get_sr_wide_cg_kernel(): + """Wide-matrix CG SR solve. Returns ``(theta, sqrt(rs), num_iter)``.""" + cached = getattr(_get_sr_wide_cg_kernel, "_cached", None) + if cached is not None: + return cached + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + + @partial( + jax.shard_map, + mesh=mesh, + in_specs=( + PSpec(None, "rank"), # X (P, N_local) + PSpec("rank"), # F (N_local,) + PSpec(), # epsilon scalar + PSpec(), # max_iter scalar + PSpec(), # tol scalar + PSpec(), # x0 (P,) replicated + ), + out_specs=(PSpec(), PSpec(), PSpec()), + ) + def _solve(X, F, epsilon, max_iter, tol, x0): + # b = psum(X F) + b = jax.lax.psum(X @ F, "rank") # (P,) + + def apply_A(v): + # v replicated across ranks; X.T @ v is local; X @ ... is local. + local = X @ (X.T @ v) + return jax.lax.psum(local, "rank") + epsilon * v + + return _cg_while_loop(b, apply_A, x0, max_iter, tol, X.dtype) + + _get_sr_wide_cg_kernel._cached = _solve + return _solve + + +def _get_sr_tall_direct_kernel(): + """Tall-matrix direct SR solve via push-through identity. + + Solves ``(X^T X + eps I) y = F`` (smaller system in sample space) and + returns ``theta = X y`` replicated across ranks. + """ + cached = getattr(_get_sr_tall_direct_kernel, "_cached", None) + if cached is not None: + return cached + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + + @partial( + jax.shard_map, + mesh=mesh, + in_specs=(PSpec(None, "rank"), PSpec("rank"), PSpec()), + out_specs=PSpec(), + # all_gather produces values that are replicated by construction but + # JAX cannot statically prove this on a 1-axis mesh; disable the check. + check_vma=False, + ) + def _solve(X, F, epsilon): + # Gather all sample columns onto every rank. In the tall regime + # ``num_samples_total`` is small by construction, so the replicated + # ``(P, N_total)`` matrix is affordable; the solve over ``N_total`` + # is the cheap dimension. + X_full = jax.lax.all_gather(X, "rank", axis=1, tiled=True) # (P, N_total) + F_full = jax.lax.all_gather(F, "rank", tiled=True) # (N_total,) + XTX = X_full.T @ X_full + XTX = XTX + epsilon * jnp.eye(XTX.shape[0], dtype=XTX.dtype) + y = jnp.linalg.solve(XTX, F_full) + return X_full @ y + + _get_sr_tall_direct_kernel._cached = _solve + return _solve + + +def _get_sr_tall_cg_kernel(): + """Tall-matrix CG SR solve via push-through identity.""" + cached = getattr(_get_sr_tall_cg_kernel, "_cached", None) + if cached is not None: + return cached + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + + @partial( + jax.shard_map, + mesh=mesh, + in_specs=( + PSpec(None, "rank"), # X (P, N_local) + PSpec("rank"), # F (N_local,) + PSpec(), # epsilon + PSpec(), # max_iter + PSpec(), # tol + PSpec(), # x0 (N_total,) replicated + ), + out_specs=(PSpec(), PSpec(), PSpec(), PSpec()), + check_vma=False, # all_gather output is replicated but not statically inferrable + ) + def _solve(X, F, epsilon, max_iter, tol, x0): + X_full = jax.lax.all_gather(X, "rank", axis=1, tiled=True) + F_full = jax.lax.all_gather(F, "rank", tiled=True) + + def apply_A(v): + return X_full.T @ (X_full @ v) + epsilon * v + + y, residual, num_iter = _cg_while_loop(F_full, apply_A, x0, max_iter, tol, X.dtype) + # Return y (sample-space CG solution) too so the caller can persist + # it as a warm-start for the next optimization step. + return X_full @ y, y, residual, num_iter + + _get_sr_tall_cg_kernel._cached = _solve + return _solve + + class MCMC: """Production VMC/MCMC driver with multiple walkers. @@ -2405,6 +2621,131 @@ def solve_linear_method( return c_vec, E_lm + @staticmethod + def _shard_X_F(X_local: npt.NDArray, F_local: npt.NDArray): + """Convert host-local NumPy ``(X, F)`` into shard_map-ready ``jax.Array`` s. + + ``X_local`` is sharded along axis 1 (samples), ``F_local`` along axis 0. + Single-process: returns plain ``jnp.array`` (1-rank mesh = identity). + Multi-process: stitches host-local slices into a global ``jax.Array``. + """ + X_jnp = jnp.asarray(X_local) + F_jnp = jnp.asarray(F_local) + if jax.process_count() <= 1: + return X_jnp, F_jnp + + from jax.sharding import NamedSharding + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + X_global = jax.make_array_from_process_local_data(NamedSharding(mesh, PSpec(None, "rank")), X_jnp) + F_global = jax.make_array_from_process_local_data(NamedSharding(mesh, PSpec("rank")), F_jnp) + return X_global, F_global + + @staticmethod + def _replicated_jax_array(arr: npt.NDArray): + """Wrap a host-local NumPy array as a replicated ``jax.Array`` across ranks. + + For multi-process runs the ``arr`` must be identical on every rank (e.g. + a CG warm-start state that was psum'd before being persisted). + """ + jnp_arr = jnp.asarray(arr) + if jax.process_count() <= 1: + return jnp_arr + + from jax.sharding import NamedSharding + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + return jax.make_array_from_process_local_data(NamedSharding(mesh, PSpec()), jnp_arr) + + def _sr_solve_wide_direct_device( + self, + X_local: npt.NDArray, + F_local: npt.NDArray, + epsilon: float, + ) -> npt.NDArray: + """Wide-matrix direct SR solve via shard_map + psum + ``jnp.linalg.solve``. + + Args: + X_local: Local (diag_S-normalized) design matrix ``(P, N_local)``. + F_local: Local right-hand-side ``(N_local,)``. + epsilon: Tikhonov regularization scalar. + + Returns: + ``theta = (X X^T + eps I)^{-1} (X F)`` of shape ``(P,)``, + identical on every rank. + """ + solver = _get_sr_wide_direct_kernel() + X_g, F_g = self._shard_X_F(X_local, F_local) + eps_jnp = jnp.asarray(epsilon, dtype=X_g.dtype) + theta = solver(X_g, F_g, eps_jnp) + theta.block_until_ready() + return np.asarray(theta) + + def _sr_solve_wide_cg_device( + self, + X_local: npt.NDArray, + F_local: npt.NDArray, + epsilon: float, + max_iter: int, + tol: float, + x0: npt.NDArray, + ) -> tuple[npt.NDArray, float, int]: + """Wide-matrix CG SR solve. Returns ``(theta, residual, num_iter)``. + + ``x0`` must be ``(P,)`` and identical on every rank (warm-start carried + across optimization iterations). + """ + solver = _get_sr_wide_cg_kernel() + X_g, F_g = self._shard_X_F(X_local, F_local) + x0_g = self._replicated_jax_array(np.asarray(x0, dtype=X_local.dtype)) + eps_jnp = jnp.asarray(epsilon, dtype=X_g.dtype) + max_iter_jnp = jnp.asarray(int(max_iter), dtype=jnp.int32) + tol_jnp = jnp.asarray(tol, dtype=X_g.dtype) + theta, residual, num_iter = solver(X_g, F_g, eps_jnp, max_iter_jnp, tol_jnp, x0_g) + theta.block_until_ready() + return np.asarray(theta), float(residual), int(num_iter) + + def _sr_solve_tall_direct_device( + self, + X_local: npt.NDArray, + F_local: npt.NDArray, + epsilon: float, + ) -> npt.NDArray: + """Tall-matrix direct SR solve (push-through identity).""" + solver = _get_sr_tall_direct_kernel() + X_g, F_g = self._shard_X_F(X_local, F_local) + eps_jnp = jnp.asarray(epsilon, dtype=X_g.dtype) + theta = solver(X_g, F_g, eps_jnp) + theta.block_until_ready() + return np.asarray(theta) + + def _sr_solve_tall_cg_device( + self, + X_local: npt.NDArray, + F_local: npt.NDArray, + epsilon: float, + max_iter: int, + tol: float, + x0: npt.NDArray, + ) -> tuple[npt.NDArray, npt.NDArray, float, int]: + """Tall-matrix CG SR solve. Returns ``(theta, y, residual, num_iter)``. + + ``x0`` and ``y`` live in the sample space, shape ``(N_total,)``; + ``y`` is the CG solution (suitable as warm-start next iteration). + ``theta = X y`` lives in parameter space, shape ``(P,)``. + """ + solver = _get_sr_tall_cg_kernel() + X_g, F_g = self._shard_X_F(X_local, F_local) + x0_g = self._replicated_jax_array(np.asarray(x0, dtype=X_local.dtype)) + eps_jnp = jnp.asarray(epsilon, dtype=X_g.dtype) + max_iter_jnp = jnp.asarray(int(max_iter), dtype=jnp.int32) + tol_jnp = jnp.asarray(tol, dtype=X_g.dtype) + theta, y, residual, num_iter = solver(X_g, F_g, eps_jnp, max_iter_jnp, tol_jnp, x0_g) + theta.block_until_ready() + return np.asarray(theta), np.asarray(y), float(residual), int(num_iter) + def run_optimize( self, num_mcmc_steps: int = 100, @@ -2424,6 +2765,7 @@ def run_optimize( opt_lambda_basis_exp: bool = False, opt_lambda_basis_coeff: bool = False, optimizer_kwargs: dict | None = None, + use_device_collectives: bool = False, ): """Optimize wavefunction parameters using SR or optax. @@ -2457,6 +2799,13 @@ def run_optimize( ``use_lm=True`` enables LM with keys (``lm_subspace_dim``, ``lm_cond``); other ``method`` names are optax constructors (e.g., ``"adam"``) and receive remaining keys. + use_device_collectives (bool, optional): If True, run the SR + direct-solve cross-rank reductions and linear solve via + ``jax.shard_map`` + ``jax.lax.psum`` instead of + ``mpi_comm.Reduce`` + ``scipy.linalg.solve``. Currently + only the wide-matrix direct path is migrated; CG and tall + paths still use the host/mpi4py code regardless of this + flag. Defaults to False (legacy CPU path). Notes: - Persists optax optimizer state across calls when method and hyperparameters match. @@ -3101,6 +3450,17 @@ def _conjugate_gradient_numpy( logger.info(f"The number of total samples is {num_samples_total}.") logger.info(f"SR matrix dimension: {num_params} x {num_params}.") + # Announce which SR solve path will be used for this iteration. + _sr_regime = "wide" if num_params < num_samples_total else "tall" + _sr_method = "CG" if sr_cg_flag else "direct" + _sr_backend = "device (jax.shard_map + psum/all_gather)" if use_device_collectives else "CPU (mpi4py + scipy)" + logger.debug( + "SR solver path: regime=%s, method=%s, backend=%s", + _sr_regime, + _sr_method, + _sr_backend, + ) + # make the SR matrix scale-invariant (i.e., normalize) ## compute X_w@X.T diag_S_local = np.einsum("jk,kj->j", X_local, X_local.T) @@ -3156,240 +3516,341 @@ def _conjugate_gradient_numpy( logger.debug("X is a wide matrix. Proceed w/o the push-through identity.") logger.debug("theta = (S+epsilon*I)^{-1}*f = (X * X^T + epsilon*I)^{-1} * X F...") if not sr_cg_flag: - logger.info("Using the direct solver for the inverse of S.") - logger.debug( - f"Estimated X_local @ X_local.T.bytes per MPI = {X_local.shape[0] ** 2 * X_local.dtype.itemsize / (2**30)} gib." - ) - # compute local sum of X * X^T - X_X_T_local = X_local @ X_local.T - logger.devel(f"X_X_T_local.shape = {X_X_T_local.shape}.") - # compute global sum of X * X^T - if mpi_rank == 0: - X_X_T = np.empty(X_X_T_local.shape, dtype=dtype_mcmc_np) - else: - X_X_T = None - mpi_comm.Reduce(X_X_T_local, X_X_T, op=MPI.SUM, root=0) - # compute local sum of X @ F - X_F_local = X_local @ F_local # shape (num_param, ) - logger.devel(f"X_F_local.shape = {X_F_local.shape}.") - # compute global sum of X @ F - if mpi_rank == 0: - X_F = np.empty(X_F_local.shape, dtype=dtype_mcmc_np) - else: - X_F = None - mpi_comm.Reduce(X_F_local, X_F, op=MPI.SUM, root=0) - # compute theta - if mpi_rank == 0: - logger.devel(f"X @ X.T.shape = {X_X_T.shape}.") - logger.devel(f"X @ F.shape = {X_F.shape}.") - # (X X^T + eps*I) x = X F ->solve-> x = (X X^T + eps*I)^{-1} X F - X_X_T[np.diag_indices_from(X_X_T)] += epsilon - - X_X_T_inv_X_F = scipy.linalg.solve(X_X_T, X_F, assume_a="sym") - # theta = (X_w X^T + eps*I)^{-1} X_w F - theta_all = X_X_T_inv_X_F + if use_device_collectives: + logger.info("Using the direct solver for the inverse of S (device-resident, shard_map + psum).") + logger.debug( + f"Estimated X_local @ X_local.T.bytes per MPI = {X_local.shape[0] ** 2 * X_local.dtype.itemsize / (2**30)} gib." + ) + theta_all = self._sr_solve_wide_direct_device( + X_local=X_local, + F_local=F_local, + epsilon=epsilon, + ) + logger.devel(f"[device] theta_all (w/o the push through identity) = {theta_all}.") + logger.devel( + f"[device] theta_all (w/o the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) else: - theta_all = None - # Broadcast theta_all to all ranks - theta_all = mpi_comm.bcast(theta_all, root=0) - logger.devel(f"[new] theta_all (w/o the push through identity) = {theta_all}.") - logger.devel( - f"[new] theta_all (w/o the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." - ) + logger.info("Using the direct solver for the inverse of S.") + logger.debug( + f"Estimated X_local @ X_local.T.bytes per MPI = {X_local.shape[0] ** 2 * X_local.dtype.itemsize / (2**30)} gib." + ) + # compute local sum of X * X^T + X_X_T_local = X_local @ X_local.T + logger.devel(f"X_X_T_local.shape = {X_X_T_local.shape}.") + # compute global sum of X * X^T + if mpi_rank == 0: + X_X_T = np.empty(X_X_T_local.shape, dtype=dtype_mcmc_np) + else: + X_X_T = None + mpi_comm.Reduce(X_X_T_local, X_X_T, op=MPI.SUM, root=0) + # compute local sum of X @ F + X_F_local = X_local @ F_local # shape (num_param, ) + logger.devel(f"X_F_local.shape = {X_F_local.shape}.") + # compute global sum of X @ F + if mpi_rank == 0: + X_F = np.empty(X_F_local.shape, dtype=dtype_mcmc_np) + else: + X_F = None + mpi_comm.Reduce(X_F_local, X_F, op=MPI.SUM, root=0) + # compute theta + if mpi_rank == 0: + logger.devel(f"X @ X.T.shape = {X_X_T.shape}.") + logger.devel(f"X @ F.shape = {X_F.shape}.") + # (X X^T + eps*I) x = X F ->solve-> x = (X X^T + eps*I)^{-1} X F + X_X_T[np.diag_indices_from(X_X_T)] += epsilon + + X_X_T_inv_X_F = scipy.linalg.solve(X_X_T, X_F, assume_a="sym") + # theta = (X_w X^T + eps*I)^{-1} X_w F + theta_all = X_X_T_inv_X_F + else: + theta_all = None + # Broadcast theta_all to all ranks + theta_all = mpi_comm.bcast(theta_all, root=0) + logger.devel(f"[new] theta_all (w/o the push through identity) = {theta_all}.") + logger.devel( + f"[new] theta_all (w/o the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) else: - logger.info("Using conjugate gradient for the inverse of S.") - logger.info(f" [CG] threshold {sr_cg_tol}.") - logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") - # conjugate gradient solver - # Compute b = X @ F (distributed) - X_F_local = X_local @ F_local # shape (num_param, ) - X_F = np.zeros_like(X_F_local) - mpi_comm.Allreduce(X_F_local, X_F, op=MPI.SUM) - - def apply_S_primal_numpy(v): - XTv_local = X_local.T @ v - XXTv_local = X_local @ XTv_local - XXTv_global = np.empty_like(XXTv_local) - mpi_comm.Allreduce(XXTv_local, XXTv_global, op=MPI.SUM) - return XXTv_global + epsilon * v - - if sr_cg_warm_start_primal is not None and sr_cg_warm_start_primal.shape == X_F.shape: - x0 = sr_cg_warm_start_primal + if use_device_collectives: + logger.info("Using conjugate gradient for the inverse of S (device-resident, shard_map + psum).") + logger.info(f" [CG] threshold {sr_cg_tol}.") + logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") + num_params_local = X_local.shape[0] + if sr_cg_warm_start_primal is not None and sr_cg_warm_start_primal.shape == (num_params_local,): + x0 = sr_cg_warm_start_primal + else: + x0 = np.zeros(num_params_local, dtype=dtype_mcmc_np) + theta_all, final_residual, num_steps = self._sr_solve_wide_cg_device( + X_local=X_local, + F_local=F_local, + epsilon=epsilon, + max_iter=sr_cg_max_iter, + tol=sr_cg_tol, + x0=x0, + ) + sr_cg_warm_start_primal = np.array(theta_all, copy=True) + logger.devel(f" [CG] Final residual: {final_residual:.3e}") + logger.info(f" [CG] Converged in {num_steps} steps") + if num_steps == sr_cg_max_iter: + logger.info(" [CG] Conjugate gradient did not converge!!") + logger.devel(f"[device/cg] theta_all (w/o the push through identity) = {theta_all}.") + logger.devel(f"[device/cg] theta_all: min, max = {np.min(theta_all)}, {np.max(theta_all)}.") else: - x0 = np.zeros_like(X_F) - - theta_all, final_residual, num_steps = _conjugate_gradient_numpy( - np.asarray(X_F, dtype=dtype_mcmc_np), - apply_S_primal_numpy, - np.asarray(x0, dtype=dtype_mcmc_np), - sr_cg_max_iter, - sr_cg_tol, - ) - sr_cg_warm_start_primal = np.array(theta_all, copy=True) - logger.devel(f" [CG] Final residual: {final_residual:.3e}") - logger.info(f" [CG] Converged in {num_steps} steps") - if num_steps == sr_cg_max_iter: - logger.info(" [CG] Conjugate gradient did not converge!!") - logger.devel(f"[new/cg] theta_all (w/o the push through identity) = {theta_all}.") - logger.devel( - f"[new/cg] theta_all (w/o the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." - ) + logger.info("Using conjugate gradient for the inverse of S.") + logger.info(f" [CG] threshold {sr_cg_tol}.") + logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") + # conjugate gradient solver + # Compute b = X @ F (distributed) + X_F_local = X_local @ F_local # shape (num_param, ) + X_F = np.zeros_like(X_F_local) + mpi_comm.Allreduce(X_F_local, X_F, op=MPI.SUM) + + def apply_S_primal_numpy(v): + XTv_local = X_local.T @ v + XXTv_local = X_local @ XTv_local + XXTv_global = np.empty_like(XXTv_local) + mpi_comm.Allreduce(XXTv_local, XXTv_global, op=MPI.SUM) + return XXTv_global + epsilon * v + + if sr_cg_warm_start_primal is not None and sr_cg_warm_start_primal.shape == X_F.shape: + x0 = sr_cg_warm_start_primal + else: + x0 = np.zeros_like(X_F) + + theta_all, final_residual, num_steps = _conjugate_gradient_numpy( + np.asarray(X_F, dtype=dtype_mcmc_np), + apply_S_primal_numpy, + np.asarray(x0, dtype=dtype_mcmc_np), + sr_cg_max_iter, + sr_cg_tol, + ) + sr_cg_warm_start_primal = np.array(theta_all, copy=True) + logger.devel(f" [CG] Final residual: {final_residual:.3e}") + logger.info(f" [CG] Converged in {num_steps} steps") + if num_steps == sr_cg_max_iter: + logger.info(" [CG] Conjugate gradient did not converge!!") + logger.devel(f"[new/cg] theta_all (w/o the push through identity) = {theta_all}.") + logger.devel( + f"[new/cg] theta_all (w/o the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) else: # num_params >= num_samples: # if True: logger.debug("X is a tall matrix. Proceed w/ the push-through identity.") logger.debug("theta = (S+epsilon*I)^{-1}*f = X(X^T * X + epsilon*I)^{-1} * F...") - # Get local shapes - N, M = X_local.shape - P = mpi_size # number of ranks - - # Compute how many rows each rank should own (distribute the remainder) - counts = [N // P + (1 if i < (N % P) else 0) for i in range(P)] - - # Compute starting row index for each rank in the original array - displs = [sum(counts[:i]) for i in range(P)] - N_local = counts[mpi_rank] # number of rows this rank will receive - - # Build send buffers by slicing X and Xw into P row-chunks - # Each chunk is flattened so we can send in one go. - sendbuf_X = np.concatenate([X_local[displs[i] : displs[i] + counts[i], :].ravel() for i in range(P)]) - - # Prepare sendcounts and displacements in units of elements - sendcounts = [counts[i] * M for i in range(P)] - sdispls = [sum(sendcounts[:i]) for i in range(P)] + if use_device_collectives: + # Device path: shard_map + all_gather handles redistribution + # internally; no Alltoallv prep on host. + if not sr_cg_flag: + logger.info( + "Using the direct solver for the inverse of S " + "(device-resident, shard_map + all_gather, push-through identity)." + ) + theta_all = self._sr_solve_tall_direct_device( + X_local=X_local, + F_local=F_local, + epsilon=epsilon, + ) + logger.devel( + f"[device] theta_all (w/ the push through identity): " + f"min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) + else: + logger.info( + "Using conjugate gradient for the inverse of S " + "(device-resident, shard_map + all_gather, push-through identity)." + ) + logger.info(f" [CG] threshold {sr_cg_tol}.") + logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") + num_samples_total_local = int(F_local.shape[0]) * mpi_size + if sr_cg_warm_start_dual is not None and sr_cg_warm_start_dual.shape == (num_samples_total_local,): + x0 = sr_cg_warm_start_dual + else: + x0 = np.zeros(num_samples_total_local, dtype=dtype_mcmc_np) + theta_all, y_sample, final_residual, num_steps = self._sr_solve_tall_cg_device( + X_local=X_local, + F_local=F_local, + epsilon=epsilon, + max_iter=sr_cg_max_iter, + tol=sr_cg_tol, + x0=x0, + ) + # Persist sample-space CG solution as warm-start for the + # next opt iteration (matches CPU branch's behavior). + sr_cg_warm_start_dual = np.array(y_sample, copy=True) + logger.devel(f" [CG] Final residual: {final_residual:.3e}") + logger.info(f" [CG] Converged in {num_steps} steps") + if num_steps == sr_cg_max_iter: + logger.info(" [CG] Conjugate gradient did not converge!!") + logger.devel( + f"[device/cg] theta_all (w/ the push through identity): " + f"min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) + # Skip the rest of the legacy CPU tall block. + # Fall through to the shared scale-back below. + # (Sentinel handled by Python control flow: the CPU branch's + # remaining code lives inside the same `else:` arm; we mirror + # by jumping past it via early-continuation pattern below.) + _device_tall_done = True + else: + _device_tall_done = False - # Prepare recvcounts and displacements: - # each rank will receive 'counts[mpi_rank]*M' elements from each of the P ranks - recvcounts = [counts[mpi_rank] * M] * P - rdispls = [i * counts[mpi_rank] * M for i in range(P)] + if _device_tall_done: + pass # device path produced theta_all already + else: + # Legacy CPU path: redistribute X via Alltoallv, then solve. + # Get local shapes + N, M = X_local.shape + P = mpi_size # number of ranks - # Allocate receive buffers - recvbuf_X = np.empty(sum(recvcounts), dtype=X_local.dtype) + # Compute how many rows each rank should own (distribute the remainder) + counts = [N // P + (1 if i < (N % P) else 0) for i in range(P)] - # Perform the all-to-all variable-sized exchange - mpi_comm.Alltoallv( - [sendbuf_X, sendcounts, sdispls, MPI.DOUBLE], [recvbuf_X, recvcounts, rdispls, MPI.DOUBLE] - ) + # Compute starting row index for each rank in the original array + displs = [sum(counts[:i]) for i in range(P)] + N_local = counts[mpi_rank] # number of rows this rank will receive - # Reshape the flat receive buffer into a 3D array - # shape = (P sources, N_local rows, M cols) - buf_X = recvbuf_X.reshape(P, N_local, M) + # Build send buffers by slicing X and Xw into P row-chunks + # Each chunk is flattened so we can send in one go. + sendbuf_X = np.concatenate([X_local[displs[i] : displs[i] + counts[i], :].ravel() for i in range(P)]) - # Rearrange into final 2D arrays of shape (N_local, M * P) - # by stacking each source's M columns side by side - X_re_local = np.hstack([buf_X[i] for i in range(P)]) # shape (num_param/P, num_mcmc * num_walker * P) - logger.devel(f"X_re_local.shape = {X_re_local.shape}.") + # Prepare sendcounts and displacements in units of elements + sendcounts = [counts[i] * M for i in range(P)] + sdispls = [sum(sendcounts[:i]) for i in range(P)] - if not sr_cg_flag: - logger.info("Using the direct solver for the inverse of S.") - logger.devel( - f"Estimated X_local.T @ X_local.bytes per MPI = {X_re_local.shape[1] ** 2 * X_re_local.dtype.itemsize / (2**30)} gib." - ) - # compute local sum of X^T * X - X_T_X_local = X_re_local.T @ X_re_local - logger.devel(f"X_T_X_local.shape = {X_T_X_local.shape}.") - # compute global sum of X^T * X - if mpi_rank == 0: - X_T_X = np.empty(X_T_X_local.shape, dtype=dtype_mcmc_np) - else: - X_T_X = None - mpi_comm.Reduce(X_T_X_local, X_T_X, op=MPI.SUM, root=0) - # gather F_local from all ranks (concatenation, not element-wise sum) - F_local_count = F_local.shape[0] - F_recvcounts = mpi_comm.gather(F_local_count, root=0) - if mpi_rank == 0: - F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] - F = np.empty(sum(F_recvcounts), dtype=dtype_mcmc_np) - else: - F_displs = None - F = None - mpi_comm.Gatherv( - [F_local, MPI.DOUBLE], - [F, (F_recvcounts, F_displs), MPI.DOUBLE] if mpi_rank == 0 else [F, None], - root=0, - ) - if mpi_rank == 0: - logger.devel(f"X_T_X.shape = {X_T_X.shape}.") - logger.devel(f"F.shape = {F.shape}.") - X_T_X[np.diag_indices_from(X_T_X)] += epsilon - # (X^T X_w + eps*I) x = F ->solve-> x = (X^T X_w + eps*I)^{-1} F - X_T_X_inv_F = scipy.linalg.solve(X_T_X, F, assume_a="sym") - K = X_T_X_inv_F.shape[0] // mpi_size - else: - X_T_X_inv_F = None - K = None - # Broadcast K to all ranks so they know how big each chunk is - K = mpi_comm.bcast(K, root=0) + # Prepare recvcounts and displacements: + # each rank will receive 'counts[mpi_rank]*M' elements from each of the P ranks + recvcounts = [counts[mpi_rank] * M] * P + rdispls = [i * counts[mpi_rank] * M for i in range(P)] - X_T_X_inv_F_local = np.empty(K, dtype=dtype_mcmc_np) + # Allocate receive buffers + recvbuf_X = np.empty(sum(recvcounts), dtype=X_local.dtype) - mpi_comm.Scatter( - [X_T_X_inv_F, MPI.DOUBLE], # send buffer (only significant on root) - X_T_X_inv_F_local, # receive buffer (on each rank) - root=0, - ) - # theta = X_w (X^T X_w + eps*I)^{-1} F - theta_all_local = X_local @ X_T_X_inv_F_local - theta_all = np.empty(theta_all_local.shape, dtype=dtype_mcmc_np) - mpi_comm.Allreduce(theta_all_local, theta_all, op=MPI.SUM) - logger.devel(f"[new] theta_all (w/ the push through identity) = {theta_all}.") - logger.devel( - f"[new] theta_all (w/ the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." - ) - else: - logger.info("Using conjugate gradient for the inverse of S.") - logger.info(f" [CG] threshold {sr_cg_tol}.") - logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") - - def apply_dual_S_numpy(v): - Xv_local = X_re_local @ v - XTXv_local = X_re_local.T @ Xv_local - XTXv_global = np.empty_like(XTXv_local) - mpi_comm.Allreduce(XTXv_local, XTXv_global, op=MPI.SUM) - return XTXv_global + epsilon * v - - # Gather F_local from all ranks (concatenation) to form F_total of length M*P - F_local_count = F_local.shape[0] - F_recvcounts = mpi_comm.allgather(F_local_count) - F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] - F_total = np.empty(sum(F_recvcounts), dtype=dtype_mcmc_np) - mpi_comm.Allgatherv( - [F_local, MPI.DOUBLE], - [F_total, (F_recvcounts, F_displs), MPI.DOUBLE], + # Perform the all-to-all variable-sized exchange + mpi_comm.Alltoallv( + [sendbuf_X, sendcounts, sdispls, MPI.DOUBLE], [recvbuf_X, recvcounts, rdispls, MPI.DOUBLE] ) - if sr_cg_warm_start_dual is not None and sr_cg_warm_start_dual.shape == F_total.shape: - x0 = sr_cg_warm_start_dual - else: - x0 = np.zeros_like(F_total) - x_sol, final_residual, num_steps = _conjugate_gradient_numpy( - F_total, - apply_dual_S_numpy, - np.asarray(x0, dtype=dtype_mcmc_np), - sr_cg_max_iter, - sr_cg_tol, - ) - sr_cg_warm_start_dual = np.array(x_sol, copy=True) - # theta = X @ x_sol, evaluated locally over X_re_local (N_local rows) - theta_local = X_re_local @ x_sol # shape (N_local,) - theta_local = np.asarray(theta_local) - N_local = theta_local.shape[0] + # Reshape the flat receive buffer into a 3D array + # shape = (P sources, N_local rows, M cols) + buf_X = recvbuf_X.reshape(P, N_local, M) - recvcounts = mpi_comm.allgather(N_local) - displs = [sum(recvcounts[:i]) for i in range(mpi_comm.Get_size())] + # Rearrange into final 2D arrays of shape (N_local, M * P) + # by stacking each source's M columns side by side + X_re_local = np.hstack([buf_X[i] for i in range(P)]) # shape (num_param/P, num_mcmc * num_walker * P) + logger.devel(f"X_re_local.shape = {X_re_local.shape}.") - theta_all = np.empty(sum(recvcounts), dtype=theta_local.dtype) - mpi_comm.Allgatherv([theta_local, MPI.DOUBLE], [theta_all, (recvcounts, displs), MPI.DOUBLE]) - - logger.devel(f" [CG] Final residual: {final_residual:.3e}") - logger.info(f" [CG] Converged in {num_steps} steps") - if num_steps == sr_cg_max_iter: - logger.logger(" [CG] Conjugate gradient did not converge!") - logger.devel(f"[new/cg] theta_all (w/o the push through identity) = {theta_all}.") - logger.devel( - f"[new/cg] theta_all (w/ the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." - ) + if not sr_cg_flag: + logger.info("Using the direct solver for the inverse of S.") + logger.devel( + f"Estimated X_local.T @ X_local.bytes per MPI = {X_re_local.shape[1] ** 2 * X_re_local.dtype.itemsize / (2**30)} gib." + ) + # compute local sum of X^T * X + X_T_X_local = X_re_local.T @ X_re_local + logger.devel(f"X_T_X_local.shape = {X_T_X_local.shape}.") + # compute global sum of X^T * X + if mpi_rank == 0: + X_T_X = np.empty(X_T_X_local.shape, dtype=dtype_mcmc_np) + else: + X_T_X = None + mpi_comm.Reduce(X_T_X_local, X_T_X, op=MPI.SUM, root=0) + # gather F_local from all ranks (concatenation, not element-wise sum) + F_local_count = F_local.shape[0] + F_recvcounts = mpi_comm.gather(F_local_count, root=0) + if mpi_rank == 0: + F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] + F = np.empty(sum(F_recvcounts), dtype=dtype_mcmc_np) + else: + F_displs = None + F = None + mpi_comm.Gatherv( + [F_local, MPI.DOUBLE], + [F, (F_recvcounts, F_displs), MPI.DOUBLE] if mpi_rank == 0 else [F, None], + root=0, + ) + if mpi_rank == 0: + logger.devel(f"X_T_X.shape = {X_T_X.shape}.") + logger.devel(f"F.shape = {F.shape}.") + X_T_X[np.diag_indices_from(X_T_X)] += epsilon + # (X^T X_w + eps*I) x = F ->solve-> x = (X^T X_w + eps*I)^{-1} F + X_T_X_inv_F = scipy.linalg.solve(X_T_X, F, assume_a="sym") + K = X_T_X_inv_F.shape[0] // mpi_size + else: + X_T_X_inv_F = None + K = None + # Broadcast K to all ranks so they know how big each chunk is + K = mpi_comm.bcast(K, root=0) + + X_T_X_inv_F_local = np.empty(K, dtype=dtype_mcmc_np) + + mpi_comm.Scatter( + [X_T_X_inv_F, MPI.DOUBLE], # send buffer (only significant on root) + X_T_X_inv_F_local, # receive buffer (on each rank) + root=0, + ) + # theta = X_w (X^T X_w + eps*I)^{-1} F + theta_all_local = X_local @ X_T_X_inv_F_local + theta_all = np.empty(theta_all_local.shape, dtype=dtype_mcmc_np) + mpi_comm.Allreduce(theta_all_local, theta_all, op=MPI.SUM) + logger.devel(f"[new] theta_all (w/ the push through identity) = {theta_all}.") + logger.devel( + f"[new] theta_all (w/ the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) + else: + logger.info("Using conjugate gradient for the inverse of S.") + logger.info(f" [CG] threshold {sr_cg_tol}.") + logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") + + def apply_dual_S_numpy(v): + Xv_local = X_re_local @ v + XTXv_local = X_re_local.T @ Xv_local + XTXv_global = np.empty_like(XTXv_local) + mpi_comm.Allreduce(XTXv_local, XTXv_global, op=MPI.SUM) + return XTXv_global + epsilon * v + + # Gather F_local from all ranks (concatenation) to form F_total of length M*P + F_local_count = F_local.shape[0] + F_recvcounts = mpi_comm.allgather(F_local_count) + F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] + F_total = np.empty(sum(F_recvcounts), dtype=dtype_mcmc_np) + mpi_comm.Allgatherv( + [F_local, MPI.DOUBLE], + [F_total, (F_recvcounts, F_displs), MPI.DOUBLE], + ) + if sr_cg_warm_start_dual is not None and sr_cg_warm_start_dual.shape == F_total.shape: + x0 = sr_cg_warm_start_dual + else: + x0 = np.zeros_like(F_total) + x_sol, final_residual, num_steps = _conjugate_gradient_numpy( + F_total, + apply_dual_S_numpy, + np.asarray(x0, dtype=dtype_mcmc_np), + sr_cg_max_iter, + sr_cg_tol, + ) + sr_cg_warm_start_dual = np.array(x_sol, copy=True) + + # theta = X @ x_sol, evaluated locally over X_re_local (N_local rows) + theta_local = X_re_local @ x_sol # shape (N_local,) + theta_local = np.asarray(theta_local) + N_local = theta_local.shape[0] + + recvcounts = mpi_comm.allgather(N_local) + displs = [sum(recvcounts[:i]) for i in range(mpi_comm.Get_size())] + + theta_all = np.empty(sum(recvcounts), dtype=theta_local.dtype) + mpi_comm.Allgatherv([theta_local, MPI.DOUBLE], [theta_all, (recvcounts, displs), MPI.DOUBLE]) + + logger.devel(f" [CG] Final residual: {final_residual:.3e}") + logger.info(f" [CG] Converged in {num_steps} steps") + if num_steps == sr_cg_max_iter: + logger.logger(" [CG] Conjugate gradient did not converge!") + logger.devel(f"[new/cg] theta_all (w/o the push through identity) = {theta_all}.") + logger.devel( + f"[new/cg] theta_all (w/ the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) # theta, back to the original scale theta_all = theta_all / np.sqrt(diag_S) diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index 5f8149ab..77466063 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -48,6 +48,7 @@ sys.path.insert(0, project_root) from jqmc._precision import get_tolerance_min +from jqmc._setting import atol_consistency, rtol_consistency from jqmc.determinant import Geminal_data from jqmc.hamiltonians import Hamiltonian_data from jqmc.jastrow_factor import ( @@ -933,6 +934,243 @@ def fake_get_gF( jax.clear_caches() +@pytest.mark.parametrize( + "regime,cg_flag", + [ + ("wide", False), + ("wide", True), + ("tall", False), + ("tall", True), + ], +) +@pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvtz_cart.h5"]) +def test_sr_device_matches_cpu(trexio_file, regime, cg_flag, monkeypatch): + """Each of the four SR solve paths (wide/tall x direct/CG) must produce + the same parameter update on the JAX-native device branch as on the + legacy NumPy/SciPy/mpi4py CPU branch, given identical inputs. + + Single-process: ``psum`` is trivial and the device path reduces to its + local computation; this test verifies numerical agreement only. + Multi-rank validation requires actually running ``mpirun`` and is out of + scope for the unit suite. + """ + from mpi4py import MPI as _MPI + + if _MPI.COMM_WORLD.Get_size() != 1: + pytest.skip("Numerical-agreement test runs single-process only.") + + ( + structure_data, + _, + _, + _, + geminal_mo_data, + coulomb_potential_data, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), store_tuple=True + ) + + jastrow_onebody_data = Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=1.0, + structure_data=structure_data, + core_electrons=tuple([0] * len(structure_data.atomic_numbers)), + jastrow_1b_type="pade", + ) + jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="pade") + jastrow_data = Jastrow_data( + jastrow_one_body_data=jastrow_onebody_data, + jastrow_two_body_data=jastrow_twobody_data, + jastrow_three_body_data=None, + jastrow_nn_data=None, + ) + wavefunction_data = Wavefunction_data(jastrow_data=jastrow_data, geminal_data=geminal_mo_data) + hamiltonian_data = Hamiltonian_data( + structure_data=structure_data, + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + ) + + num_walkers = 2 + Dt = 2.0 + mcmc_seed = 12345 + epsilon_AS = 1.0e-6 + + # Build a parameter set sized for the requested regime. Single-process, + # so num_samples_total = num_mcmc * num_walkers. + base_params: dict[str, np.ndarray] = { + "j1_param": np.ones_like(np.array(jastrow_onebody_data.jastrow_1b_param)), + "j2_param": np.ones_like(np.array(jastrow_twobody_data.jastrow_2b_param)), + } + fixed_param_size = sum(v.size for v in base_params.values()) + + if regime == "tall": + num_mcmc = 1 + min_samples_total = num_mcmc * num_walkers + # Pad lambda_matrix so num_params >= num_samples_total. + lambda_size_needed = max(1, min_samples_total - fixed_param_size + 1) + base_params["lambda_matrix"] = np.ones(lambda_size_needed, dtype=float) + else: + base_params["lambda_matrix"] = np.array([[2.0, -2.0], [3.0, -3.0]], dtype=float) + total_params_tmp = sum(v.size for v in base_params.values()) + num_mcmc = total_params_tmp // num_walkers + 2 + + total_params = sum(v.size for v in base_params.values()) + num_samples_total = num_mcmc * num_walkers + if regime == "wide": + assert total_params < num_samples_total, f"wide setup invalid: {total_params} >= {num_samples_total}" + else: + assert total_params >= num_samples_total, f"tall setup invalid: {total_params} < {num_samples_total}" + + rng = np.random.default_rng(42) + fake_w_L_data = np.ones((num_mcmc, num_walkers)) + fake_e_L_data = rng.standard_normal((num_mcmc, num_walkers)) * 0.1 + + params_registry: dict[int, dict[str, np.ndarray]] = {} + + def register_params(wf, params): + params_registry[id(wf)] = params + + def lookup_params(wf): + return params_registry[id(wf)] + + def fake_get_variational_blocks( + self, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=True, + opt_JNN_param=True, + opt_lambda_param=False, + opt_J3_basis_exp=False, + opt_J3_basis_coeff=False, + opt_lambda_basis_exp=False, + opt_lambda_basis_coeff=False, + ): + blocks = [] + pos = lookup_params(self) + if opt_J1_param and "j1_param" in pos: + arr = pos["j1_param"] + blocks.append(VariationalParameterBlock(name="j1_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_J2_param and "j2_param" in pos: + arr = pos["j2_param"] + blocks.append(VariationalParameterBlock(name="j2_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_lambda_param and "lambda_matrix" in pos: + arr = pos["lambda_matrix"] + blocks.append(VariationalParameterBlock(name="lambda_matrix", values=arr, shape=arr.shape, size=int(arr.size))) + return blocks + + def fake_apply_block_updates(self, blocks, thetas, learning_rate): + params = lookup_params(self) + idx = 0 + for block in blocks: + blk_slice = thetas[idx : idx + block.size] + idx += block.size + if blk_slice.size == 0: + continue + delta = blk_slice.reshape(block.shape) + params[block.name] = params[block.name] + learning_rate * delta + return self + + def fake_run(self, num_mcmc_steps: int = 0, max_time=None): + return None + + # Deterministic O matrix so both runs see identical inputs. + def fake_get_dln_WF( + self, + blocks, + num_mcmc_warmup_steps=0, + chosen_param_index=None, + lambda_projectors=None, + num_orb_projection=None, + ): + total = sum(block.size for block in blocks) + rng_local = np.random.default_rng(123) + return rng_local.standard_normal((num_mcmc, self.num_walkers, total)) * 0.01 + + def fake_get_E(self, num_mcmc_warmup_steps: int = 0, num_mcmc_bin_blocks: int = 1): + return (0.0, 0.0, 0.0, 0.0) + + def fake_get_gF( + self, + num_mcmc_warmup_steps, + num_mcmc_bin_blocks, + blocks, + lambda_projectors=None, + num_orb_projection=None, + chosen_param_index=None, + ): + total = sum(block.size for block in blocks) + return np.ones(total, dtype=float), np.ones(total, dtype=float) + + monkeypatch.setattr(Wavefunction_data, "get_variational_blocks", fake_get_variational_blocks, raising=False) + monkeypatch.setattr(Wavefunction_data, "apply_block_updates", fake_apply_block_updates, raising=False) + monkeypatch.setattr(MCMC, "run", fake_run, raising=False) + monkeypatch.setattr(MCMC, "get_E", fake_get_E, raising=False) + monkeypatch.setattr(MCMC, "get_gF", fake_get_gF, raising=False) + monkeypatch.setattr(MCMC, "get_dln_WF", fake_get_dln_WF, raising=False) + monkeypatch.setattr(MCMC, "w_L", property(lambda self: fake_w_L_data), raising=False) + monkeypatch.setattr(MCMC, "e_L", property(lambda self: fake_e_L_data), raising=False) + + def run_once(use_device_collectives: bool): + mcmc = MCMC( + hamiltonian_data=hamiltonian_data, + Dt=Dt, + mcmc_seed=mcmc_seed, + epsilon_AS=epsilon_AS, + num_walkers=num_walkers, + comput_position_deriv=False, + comput_log_WF_param_deriv=True, + comput_e_L_param_deriv=False, + random_discretized_mesh=True, + ) + params = {k: v.copy() for k, v in base_params.items()} + register_params(mcmc.hamiltonian_data.wavefunction_data, params) + mcmc.run_optimize( + num_mcmc_steps=num_mcmc, + num_opt_steps=1, + num_mcmc_warmup_steps=0, + num_mcmc_bin_blocks=1, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=False, + opt_JNN_param=False, + opt_lambda_param=True, + optimizer_kwargs={ + "method": "sr", + "delta": 1.0e-3, + "epsilon": 1.0e-3, + "cg_flag": cg_flag, + "cg_max_iter": 200, + # CG iteration tolerance well below the project consistency + # tolerance, so the device-vs-CPU difference is dominated by + # float64 round-off, not CG residual. + "cg_tol": 1.0e-12, + }, + use_device_collectives=use_device_collectives, + ) + return params + + cpu_params = run_once(use_device_collectives=False) + dev_params = run_once(use_device_collectives=True) + + # Both branches do exactly the same float64 SR arithmetic, just with + # NumPy/SciPy/mpi4py vs JAX/shard_map; difference is round-off only. + # Use the project's strict-float64 consistency tolerance. + for key in cpu_params: + cpu_v = cpu_params[key] + dev_v = dev_params[key] + # Sanity: CPU branch produced a non-trivial update. + assert not np.array_equal(cpu_v, base_params[key]), f"baseline CPU update is trivial for {key}" + np.testing.assert_allclose( + dev_v, + cpu_v, + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=f"device vs CPU mismatch for {key} (regime={regime}, cg={cg_flag})", + ) + + jax.clear_caches() + + @pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvtz_cart.h5"]) def test_opt_with_projected_MOs(trexio_file, monkeypatch): """After run_optimize with opt_with_projected_MOs=True the final wavefunction From 7c58bcafd36b9ad02c2586eef48effa0a8e4d927 Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Fri, 8 May 2026 16:48:15 +0900 Subject: [PATCH 60/97] Default `use_device_collectives=True`; add MPI/JAX consistency check - The device branch (`shard_map` + `psum` / `all_gather`) is now the default in `run_optimize`, while the CPU branch (`mpi4py` + `scipy`) is retained as a fallback via `use_device_collectives=False`. - Add `check_mpi4py_jax_distribution_consistency()` to `_jqmc_utility.py`, and call it from `MCMC.run` / `run_optimize`, `GFMC_t.run` / `GFMC_n.run`, and `jqmc_cli` to surface cases where `jax.distributed.initialize()` was forgotten. - Add a session fixture in `conftest` to initialize `jax.distributed` under `mpirun` (with proxy stripping), along with new device-vs-CPU agreement tests covering multi-rank execution, CG warm-start, and LM/aSR. --- jqmc/_jqmc_utility.py | 37 +++ jqmc/jqmc_cli.py | 8 +- jqmc/jqmc_gfmc.py | 6 +- jqmc/jqmc_mcmc.py | 30 +- tests/conftest.py | 32 ++ tests/test_jqmc_mcmc.py | 645 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 747 insertions(+), 11 deletions(-) diff --git a/jqmc/_jqmc_utility.py b/jqmc/_jqmc_utility.py index da207123..55493162 100644 --- a/jqmc/_jqmc_utility.py +++ b/jqmc/_jqmc_utility.py @@ -41,8 +41,10 @@ from functools import cache from logging import getLogger +import jax import numpy as np import numpy.typing as npt +from mpi4py import MPI # set logger logger = getLogger("jqmc").getChild(__name__) @@ -51,6 +53,41 @@ num_sep_line = 66 +def check_mpi4py_jax_distribution_consistency() -> None: + """Raise ``RuntimeError`` if ``jax.process_count()`` differs from MPI world size. + + Required precondition for any production code path that relies on + ``jax.shard_map`` + ``psum`` / ``all_gather`` to aggregate across MPI + ranks (currently the device branch of ``MCMC.run_optimize``; future + GFMC device migrations will share this requirement). Without + ``jax.distributed.initialize(cluster_detection_method="mpi4py")``, each + rank's JAX sees only its own local devices and the cross-rank + collectives silently degenerate to per-rank-local computation, producing + wrong results that don't match the global solution. + + Called at the entry point of every public driver loop (``MCMC.run``, + ``MCMC.run_optimize``, ``GFMC_t.run``, ``GFMC_n.run``) so the failure + surfaces immediately rather than after substantial work has been done + on stale per-rank state. + + The CLI (``jqmc_cli.py``) initializes JAX distributed automatically. + User scripts that import ``MCMC`` / ``GFMC`` directly must call + ``jax.distributed.initialize(cluster_detection_method="mpi4py")`` + themselves before invoking these driver loops (see ``jqmc_cli.py`` + for the standard recipe, including the proxy-strip workaround). + """ + mpi_size = MPI.COMM_WORLD.Get_size() + if mpi_size != jax.process_count(): + raise RuntimeError( + f"MPI/JAX rank-count mismatch: mpi_size={mpi_size} vs " + f"jax.process_count()={jax.process_count()}. JAX cross-rank " + "collectives (psum / all_gather) require ``jax.distributed.initialize(" + "cluster_detection_method='mpi4py')`` to be called before any " + "production driver loop. The CLI does this automatically; user " + "scripts importing MCMC / GFMC must call it themselves." + ) + + def _generate_init_electron_configurations( tot_num_electron_up: int, tot_num_electron_dn: int, diff --git a/jqmc/jqmc_cli.py b/jqmc/jqmc_cli.py index f19d569a..d752b4cc 100644 --- a/jqmc/jqmc_cli.py +++ b/jqmc/jqmc_cli.py @@ -48,7 +48,7 @@ # jQMC from ._header_footer import _print_footer, _print_header -from ._jqmc_utility import num_sep_line +from ._jqmc_utility import check_mpi4py_jax_distribution_consistency, num_sep_line from ._precision import configure as configure_precision from ._precision import mode_label as precision_mode_label from ._precision import zone_detail as precision_zone_detail @@ -233,6 +233,12 @@ def _cli(): logger.info("") jax_distributed_is_initialized = False + # Surface silently-failed distributed init: ``initialize`` may swallow + # exceptions (the try/except above) but if we are actually under + # ``mpirun -n N>=2`` and JAX still sees only 1 process, every downstream + # device-collective call would silently produce per-rank-local results. + check_mpi4py_jax_distribution_consistency() + if jax_distributed_is_initialized: # global JAX device global_device_info = jax.devices() diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 18da7468..ab86d865 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -55,7 +55,7 @@ from mpi4py import MPI from ._diff_mask import DiffMask, apply_diff_mask -from ._jqmc_utility import _generate_init_electron_configurations +from ._jqmc_utility import _generate_init_electron_configurations, check_mpi4py_jax_distribution_consistency from ._precision import get_tolerance_min from ._setting import ( GFMC_MIN_BIN_BLOCKS, @@ -651,6 +651,8 @@ def run(self, num_mcmc_steps: int = 50, max_time: int = 86400) -> None: num_branching (int): number of branching (reconfiguration of walkers). max_time (int): maximum time in sec. """ + check_mpi4py_jax_distribution_consistency() + # set timer timer_projection_init = 0.0 timer_projection_total = 0.0 @@ -4690,6 +4692,8 @@ def run(self, num_mcmc_steps: int = 50, max_time: int = 86400) -> None: num_branching (int): number of branching (reconfiguration of walkers). max_time (int): maximum time in sec. """ + check_mpi4py_jax_distribution_consistency() + # initialize numpy random seed np.random.seed(self.__mpi_seed) diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index f1574085..5e8ad004 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -58,7 +58,7 @@ from mpi4py import MPI from ._diff_mask import DiffMask, apply_diff_mask -from ._jqmc_utility import _generate_init_electron_configurations +from ._jqmc_utility import _generate_init_electron_configurations, check_mpi4py_jax_distribution_consistency from ._setting import ( MCMC_MIN_BIN_BLOCKS, MCMC_MIN_WARMUP_STEPS, @@ -677,6 +677,8 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: - Accumulates energies, weights, forces, and wavefunction gradients into public buffers (``w_L``, ``e_L``, ``dln_Psi_*`` etc.). - Logs timing statistics and acceptance ratios at the end of the run. """ + check_mpi4py_jax_distribution_consistency() + # timer_counter timer_mcmc_total = 0.0 timer_mcmc_update_init = 0.0 @@ -2765,7 +2767,7 @@ def run_optimize( opt_lambda_basis_exp: bool = False, opt_lambda_basis_coeff: bool = False, optimizer_kwargs: dict | None = None, - use_device_collectives: bool = False, + use_device_collectives: bool = True, ): """Optimize wavefunction parameters using SR or optax. @@ -2799,13 +2801,21 @@ def run_optimize( ``use_lm=True`` enables LM with keys (``lm_subspace_dim``, ``lm_cond``); other ``method`` names are optax constructors (e.g., ``"adam"``) and receive remaining keys. - use_device_collectives (bool, optional): If True, run the SR - direct-solve cross-rank reductions and linear solve via - ``jax.shard_map`` + ``jax.lax.psum`` instead of - ``mpi_comm.Reduce`` + ``scipy.linalg.solve``. Currently - only the wide-matrix direct path is migrated; CG and tall - paths still use the host/mpi4py code regardless of this - flag. Defaults to False (legacy CPU path). + use_device_collectives (bool, optional): If True (default), run + the SR cross-rank reductions and linear solve via + ``jax.shard_map`` + ``jax.lax.psum`` / ``all_gather`` (NCCL on + multi-process GPU, Gloo on multi-process CPU, trivial on + single-process). If False, fall back to ``mpi_comm.Reduce`` / + ``Allreduce`` / ``Alltoallv`` + ``scipy.linalg.solve`` (legacy + mpi4py + SciPy path). + + When True under ``mpirun -n N>=2``, ``jax.distributed.initialize( + cluster_detection_method="mpi4py")`` must have been called + before ``run_optimize``; otherwise the device path silently + produces per-rank-local results that don't match the global + solution. ``run_optimize`` raises ``RuntimeError`` if it + detects ``mpi_size != jax.process_count()`` to surface this + misconfiguration. Notes: - Persists optax optimizer state across calls when method and hyperparameters match. @@ -2850,6 +2860,8 @@ def run_optimize( if not self.__comput_e_L_param_deriv: raise RuntimeError("use_lm requires comput_e_L_param_deriv=True.") + check_mpi4py_jax_distribution_consistency() + optax_kwargs = { k: v for k, v in optimizer_kwargs.items() diff --git a/tests/conftest.py b/tests/conftest.py index 3ec4467f..d390b0d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,38 @@ import pytest +@pytest.fixture(scope="session", autouse=True) +def _jax_distributed_init(): + """Initialize ``jax.distributed`` once per pytest session under multi-rank MPI. + + The device branch of ``run_optimize`` (``use_device_collectives=True``) + relies on ``jax.lax.psum`` / ``jax.lax.all_gather`` to aggregate across + MPI ranks. Without ``jax.distributed.initialize`` each rank's JAX sees + only its own local devices (``jax.process_count() == 1``) and the + collectives degenerate to no-ops, silently producing wrong results. + + Mirrors the production CLI setup in ``jqmc_cli.py``: strip HTTP proxy + environment variables before calling ``initialize`` so the JAX gRPC + coordination service doesn't try to route the local-host connection + through a proxy (which is the typical cause of "hangs forever" on + macOS / corporate networks). + """ + import os + + from mpi4py import MPI + + if MPI.COMM_WORLD.Get_size() > 1: + # Same proxy-strip workaround as jqmc_cli.py. + for _proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(_proxy_var, None) + try: + jax.distributed.initialize(cluster_detection_method="mpi4py") + except Exception: + # already initialized in the same process, or backend cannot start + pass + yield + + def pytest_addoption(parser): """Add options for pytests.""" parser.addoption("--disable-jit", action="store_true", default=False, help="Disable jax.jit for pytests") diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index 77466063..dd4fe155 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -1171,6 +1171,651 @@ def run_once(use_device_collectives: bool): jax.clear_caches() +@pytest.mark.parametrize( + "regime,cg_flag", + [ + ("wide", False), + ("wide", True), + ("tall", False), + ("tall", True), + ], +) +@pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvtz_cart.h5"]) +def test_sr_device_matches_cpu_multirank(trexio_file, regime, cg_flag, monkeypatch): + """Multi-rank counterpart to ``test_sr_device_matches_cpu``. + + Verifies that under ``mpirun -n N>=2``: + + - The legacy CPU branch (``mpi_comm.Reduce`` / ``Allreduce`` / ``Alltoallv`` + via mpi4py) and + - The device branch (``jax.lax.psum`` / ``all_gather`` via NCCL on GPU + or Gloo on CPU, dispatched through ``shard_map``) + + produce numerically equivalent ``theta`` updates for all four SR + paths (wide/tall x direct/CG). + + Each MPI rank is given *different* fake samples (via a rank-dependent + seed) so that the cross-rank reduction has actual work to do; if the + fixture-installed ``jax.distributed.initialize`` were missing, the + device branch would silently produce per-rank-local results that + wouldn't agree with the CPU branch's globally-aggregated result. + + Skipped on single-process runs (the single-process variant lives in + ``test_sr_device_matches_cpu``). + """ + from mpi4py import MPI as _MPI + + comm = _MPI.COMM_WORLD + mpi_size = comm.Get_size() + mpi_rank = comm.Get_rank() + + if mpi_size < 2: + pytest.skip("Multi-rank agreement test requires at least 2 MPI ranks.") + if jax.process_count() < 2: + pytest.skip( + "Multi-rank agreement test requires jax.distributed to be initialized " + "(JAX sees only 1 process despite multiple MPI ranks). The conftest " + "fixture should auto-init under ``mpirun -n N pytest``; check that the " + "init didn't silently fail (proxy env vars, network sandboxing)." + ) + + ( + structure_data, + _, + _, + _, + geminal_mo_data, + coulomb_potential_data, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), store_tuple=True + ) + + jastrow_onebody_data = Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=1.0, + structure_data=structure_data, + core_electrons=tuple([0] * len(structure_data.atomic_numbers)), + jastrow_1b_type="pade", + ) + jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="pade") + jastrow_data = Jastrow_data( + jastrow_one_body_data=jastrow_onebody_data, + jastrow_two_body_data=jastrow_twobody_data, + jastrow_three_body_data=None, + jastrow_nn_data=None, + ) + wavefunction_data = Wavefunction_data(jastrow_data=jastrow_data, geminal_data=geminal_mo_data) + hamiltonian_data = Hamiltonian_data( + structure_data=structure_data, + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + ) + + num_walkers = 2 + Dt = 2.0 + mcmc_seed = 12345 + epsilon_AS = 1.0e-6 + + # Build a parameter set sized for the requested regime; use mpi_size in + # the sample-count budget since the SR system sees all ranks' samples. + base_params: dict[str, np.ndarray] = { + "j1_param": np.ones_like(np.array(jastrow_onebody_data.jastrow_1b_param)), + "j2_param": np.ones_like(np.array(jastrow_twobody_data.jastrow_2b_param)), + } + fixed_param_size = sum(v.size for v in base_params.values()) + + if regime == "tall": + num_mcmc = 1 + min_samples_total = num_mcmc * num_walkers * mpi_size + lambda_size_needed = max(1, min_samples_total - fixed_param_size + 1) + base_params["lambda_matrix"] = np.ones(lambda_size_needed, dtype=float) + else: + base_params["lambda_matrix"] = np.array([[2.0, -2.0], [3.0, -3.0]], dtype=float) + total_params_tmp = sum(v.size for v in base_params.values()) + # Ensure num_mcmc * num_walkers * mpi_size > total_params_tmp. + num_mcmc = max(1, total_params_tmp // (num_walkers * mpi_size) + 2) + + total_params = sum(v.size for v in base_params.values()) + num_samples_total = num_mcmc * num_walkers * mpi_size + if regime == "wide": + assert total_params < num_samples_total, f"wide setup invalid: {total_params} >= {num_samples_total}" + else: + assert total_params >= num_samples_total, f"tall setup invalid: {total_params} < {num_samples_total}" + + # Rank-dependent fake data: each rank sees different samples so the + # cross-rank reduction is meaningful. Same seeds in both run_once calls + # so CPU and device branches see identical inputs. + fake_w_L_data = np.ones((num_mcmc, num_walkers)) + rng = np.random.default_rng(42 + mpi_rank) + fake_e_L_data = rng.standard_normal((num_mcmc, num_walkers)) * 0.1 + + params_holder: dict[str, dict[str, np.ndarray] | None] = {"params": None} + + def register_params(_wf, params): + params_holder["params"] = params + + def lookup_params(_wf): + return params_holder["params"] + + def fake_get_variational_blocks( + self, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=True, + opt_JNN_param=True, + opt_lambda_param=False, + opt_J3_basis_exp=False, + opt_J3_basis_coeff=False, + opt_lambda_basis_exp=False, + opt_lambda_basis_coeff=False, + ): + blocks = [] + pos = lookup_params(self) + if opt_J1_param and "j1_param" in pos: + arr = pos["j1_param"] + blocks.append(VariationalParameterBlock(name="j1_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_J2_param and "j2_param" in pos: + arr = pos["j2_param"] + blocks.append(VariationalParameterBlock(name="j2_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_lambda_param and "lambda_matrix" in pos: + arr = pos["lambda_matrix"] + blocks.append(VariationalParameterBlock(name="lambda_matrix", values=arr, shape=arr.shape, size=int(arr.size))) + return blocks + + def fake_apply_block_updates(self, blocks, thetas, learning_rate): + params = lookup_params(self) + idx = 0 + for block in blocks: + blk_slice = thetas[idx : idx + block.size] + idx += block.size + if blk_slice.size == 0: + continue + delta = blk_slice.reshape(block.shape) + params[block.name] = params[block.name] + learning_rate * delta + return self + + def fake_run(self, num_mcmc_steps: int = 0, max_time=None): + return None + + def fake_get_dln_WF( + self, + blocks, + num_mcmc_warmup_steps=0, + chosen_param_index=None, + lambda_projectors=None, + num_orb_projection=None, + ): + total = sum(block.size for block in blocks) + rng_local = np.random.default_rng(123 + mpi_rank) + return rng_local.standard_normal((num_mcmc, self.num_walkers, total)) * 0.01 + + def fake_get_E(self, num_mcmc_warmup_steps: int = 0, num_mcmc_bin_blocks: int = 1): + return (0.0, 0.0, 0.0, 0.0) + + def fake_get_gF( + self, + num_mcmc_warmup_steps, + num_mcmc_bin_blocks, + blocks, + lambda_projectors=None, + num_orb_projection=None, + chosen_param_index=None, + ): + total = sum(block.size for block in blocks) + return np.ones(total, dtype=float), np.ones(total, dtype=float) + + monkeypatch.setattr(Wavefunction_data, "get_variational_blocks", fake_get_variational_blocks, raising=False) + monkeypatch.setattr(Wavefunction_data, "apply_block_updates", fake_apply_block_updates, raising=False) + monkeypatch.setattr(MCMC, "run", fake_run, raising=False) + monkeypatch.setattr(MCMC, "get_E", fake_get_E, raising=False) + monkeypatch.setattr(MCMC, "get_gF", fake_get_gF, raising=False) + monkeypatch.setattr(MCMC, "get_dln_WF", fake_get_dln_WF, raising=False) + monkeypatch.setattr(MCMC, "w_L", property(lambda self: fake_w_L_data), raising=False) + monkeypatch.setattr(MCMC, "e_L", property(lambda self: fake_e_L_data), raising=False) + + def run_once(use_device_collectives: bool): + mcmc = MCMC( + hamiltonian_data=hamiltonian_data, + Dt=Dt, + mcmc_seed=mcmc_seed, + epsilon_AS=epsilon_AS, + num_walkers=num_walkers, + comput_position_deriv=False, + comput_log_WF_param_deriv=True, + comput_e_L_param_deriv=False, + random_discretized_mesh=True, + ) + params = {k: v.copy() for k, v in base_params.items()} + register_params(mcmc.hamiltonian_data.wavefunction_data, params) + mcmc.run_optimize( + num_mcmc_steps=num_mcmc, + num_opt_steps=1, + num_mcmc_warmup_steps=0, + num_mcmc_bin_blocks=1, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=False, + opt_JNN_param=False, + opt_lambda_param=True, + optimizer_kwargs={ + "method": "sr", + "delta": 1.0e-3, + "epsilon": 1.0e-3, + "cg_flag": cg_flag, + "cg_max_iter": 200, + "cg_tol": 1.0e-14, + }, + use_device_collectives=use_device_collectives, + ) + return params + + cpu_params = run_once(use_device_collectives=False) + dev_params = run_once(use_device_collectives=True) + + # Both branches must agree to consistency tolerance on every rank. + for key in cpu_params: + cpu_v = cpu_params[key] + dev_v = dev_params[key] + assert not np.array_equal(cpu_v, base_params[key]), f"baseline CPU update is trivial for {key} (rank={mpi_rank})" + np.testing.assert_allclose( + dev_v, + cpu_v, + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=(f"device vs CPU multirank mismatch for {key} (regime={regime}, cg={cg_flag}, rank={mpi_rank}/{mpi_size})"), + ) + + # Sanity: CPU branch's bcast / device branch's psum both replicate theta + # across ranks, so the wf updates should agree across ranks too. + rank0_cpu = comm.bcast({k: v.copy() for k, v in cpu_params.items()}, root=0) + for key in cpu_params: + np.testing.assert_allclose( + cpu_params[key], + rank0_cpu[key], + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=f"CPU branch theta differs across ranks for {key} (rank={mpi_rank})", + ) + rank0_dev = comm.bcast({k: v.copy() for k, v in dev_params.items()}, root=0) + for key in dev_params: + np.testing.assert_allclose( + dev_params[key], + rank0_dev[key], + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=f"device branch theta differs across ranks for {key} (rank={mpi_rank})", + ) + + jax.clear_caches() + + +@pytest.mark.parametrize( + "lm_subspace_dim,cg_flag,num_mcmc,num_walkers", + [ + # aSR (gamma scaling): smooth function, strict at any size. + (0, False, 10, 2), + (0, True, 10, 2), + # Subspace LM (size 2 + SR collective = 3 dims): well-conditioned + # once samples >> 3, so strict at 200 mcmc * 4 walkers = 800 samples. + (2, False, 200, 4), + (2, True, 200, 4), + ], +) +def test_sr_lm_device_matches_cpu(lm_subspace_dim, cg_flag, num_mcmc, num_walkers): + """LM / aSR end-to-end optimization with ``use_device_collectives`` + toggled. + + The device branch only replaces the SR direct/CG solve; everything + downstream (``get_aH``, ``solve_linear_method``, aSR gamma) still runs + on the CPU/mpi4py path. + + Tested LM modes (cf. ``run_optimize`` ``optimizer_kwargs``): + - ``lm_subspace_dim = 0``: aSR (gamma from H_0/H_1/H_2/S_2) + - ``lm_subspace_dim = N`` (positive small): subspace LM (top-N + SR collective) + + Both modes are well-conditioned at the chosen sample sizes: + ``solve_linear_method``'s eigenvalue / argmax operations have + unique well-separated winners, so the chain SR theta -> LM matrices -> + eigvec selection is Lipschitz. Strict consistency tolerance applies. + + ``lm_subspace_dim = -1`` (full-space LM) is intentionally not tested: + the augmented H_bar matrix always has many near-degenerate eigenvalues + (from gauge freedoms / redundant parameters) so the LM solver is + non-Lipschitz to round-off in the SR theta. Full-space LM is rarely + used in practice; subspace LM and aSR cover the supported workflows. + + Single optimization step only: ``num_opt_steps > 1`` would diverge the + MCMC trajectories once round-off-level wf differences accumulate. + """ + from mpi4py import MPI as _MPI + + if _MPI.COMM_WORLD.Get_size() != 1: + pytest.skip("Numerical-agreement test runs single-process only.") + + trexio_file_path = os.path.join(os.path.dirname(__file__), "trexio_example_files", "H2_ae_ccpvdz_cart.h5") + + def build_mcmc(): + ( + structure_data, + aos_data, + _, + _, + geminal_mo_data, + coulomb_potential_data, + ) = read_trexio_file(trexio_file=trexio_file_path, store_tuple=True) + + jastrow_data = Jastrow_data( + jastrow_one_body_data=Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=1.0, + structure_data=structure_data, + core_electrons=tuple([0] * len(structure_data.atomic_numbers)), + jastrow_1b_type="pade", + ), + jastrow_two_body_data=Jastrow_two_body_data.init_jastrow_two_body_data( + jastrow_2b_param=0.5, jastrow_2b_type="pade" + ), + jastrow_three_body_data=Jastrow_three_body_data.init_jastrow_three_body_data(orb_data=aos_data), + ) + wavefunction_data = Wavefunction_data(jastrow_data=jastrow_data, geminal_data=geminal_mo_data) + hamiltonian_data = Hamiltonian_data( + structure_data=structure_data, + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + ) + return MCMC( + hamiltonian_data=hamiltonian_data, + Dt=2.0, + mcmc_seed=12345, + num_walkers=num_walkers, + comput_position_deriv=False, + comput_log_WF_param_deriv=True, + comput_e_L_param_deriv=True, # required by use_lm=True + ) + + def run_once(use_device_collectives: bool): + mcmc = build_mcmc() + mcmc.run_optimize( + num_mcmc_steps=num_mcmc, + num_opt_steps=1, + num_mcmc_warmup_steps=0, + num_mcmc_bin_blocks=1, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=True, + opt_lambda_param=True, + optimizer_kwargs={ + "method": "sr", + "use_lm": True, + "lm_subspace_dim": lm_subspace_dim, + "lm_cond": 1.0e-3, + "delta": 0.1, + "epsilon": 1.0e-6, + "cg_flag": cg_flag, + # NB: cg_tol=1e-14 (near machine eps) is needed for the + # LM step to receive bit-comparable theta_SR from both + # branches. With cg_tol=1e-12, CG can early-terminate at + # mutually different points along the iteration trajectory, + # producing O(1e-3) differences that the LM step preserves. + "cg_max_iter": 2000, + "cg_tol": 1.0e-14, + }, + use_device_collectives=use_device_collectives, + ) + wf = mcmc.hamiltonian_data.wavefunction_data + captured: dict[str, np.ndarray] = {} + if wf.jastrow_data.jastrow_one_body_data is not None: + captured["j1_param"] = np.asarray(wf.jastrow_data.jastrow_one_body_data.jastrow_1b_param) + if wf.jastrow_data.jastrow_two_body_data is not None: + captured["j2_param"] = np.asarray(wf.jastrow_data.jastrow_two_body_data.jastrow_2b_param) + if wf.jastrow_data.jastrow_three_body_data is not None: + captured["j3_matrix"] = np.asarray(wf.jastrow_data.jastrow_three_body_data.j_matrix) + if wf.geminal_data is not None: + captured["lambda_matrix"] = np.asarray(wf.geminal_data.lambda_matrix) + return captured + + cpu_params = run_once(use_device_collectives=False) + dev_params = run_once(use_device_collectives=True) + + for key in cpu_params: + np.testing.assert_allclose( + dev_params[key], + cpu_params[key], + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=(f"device vs CPU LM mismatch for {key} (lm_subspace_dim={lm_subspace_dim}, cg_flag={cg_flag})"), + ) + + jax.clear_caches() + + +@pytest.mark.parametrize("regime", ["wide", "tall"]) +@pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvtz_cart.h5"]) +def test_sr_cg_warm_start_device_matches_cpu(trexio_file, regime, monkeypatch): + """Multi-step CG with warm-start: device branch must mirror CPU branch + after multiple optimization iterations. + + Each iteration the CG solver carries the previous step's solution as the + initial guess (``sr_cg_warm_start_primal`` for wide, ``sr_cg_warm_start_dual`` + for tall). Both CPU and device branches must persist this state correctly + so the final wf parameters agree to consistency tolerance. + + To make the warm-start path actually exercise the iteration-to-iteration + carry, the fake ``O`` matrix is varied per call (using a counter that is + reset between the two ``run_once`` invocations so both branches see the + same input sequence). + """ + from mpi4py import MPI as _MPI + + if _MPI.COMM_WORLD.Get_size() != 1: + pytest.skip("Numerical-agreement test runs single-process only.") + + ( + structure_data, + _, + _, + _, + geminal_mo_data, + coulomb_potential_data, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), store_tuple=True + ) + + jastrow_onebody_data = Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=1.0, + structure_data=structure_data, + core_electrons=tuple([0] * len(structure_data.atomic_numbers)), + jastrow_1b_type="pade", + ) + jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="pade") + jastrow_data = Jastrow_data( + jastrow_one_body_data=jastrow_onebody_data, + jastrow_two_body_data=jastrow_twobody_data, + jastrow_three_body_data=None, + jastrow_nn_data=None, + ) + wavefunction_data = Wavefunction_data(jastrow_data=jastrow_data, geminal_data=geminal_mo_data) + hamiltonian_data = Hamiltonian_data( + structure_data=structure_data, + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + ) + + num_walkers = 2 + Dt = 2.0 + mcmc_seed = 12345 + epsilon_AS = 1.0e-6 + num_opt_steps = 3 + + base_params: dict[str, np.ndarray] = { + "j1_param": np.ones_like(np.array(jastrow_onebody_data.jastrow_1b_param)), + "j2_param": np.ones_like(np.array(jastrow_twobody_data.jastrow_2b_param)), + } + fixed_param_size = sum(v.size for v in base_params.values()) + + if regime == "tall": + num_mcmc = 1 + min_samples_total = num_mcmc * num_walkers + lambda_size_needed = max(1, min_samples_total - fixed_param_size + 1) + base_params["lambda_matrix"] = np.ones(lambda_size_needed, dtype=float) + else: + base_params["lambda_matrix"] = np.array([[2.0, -2.0], [3.0, -3.0]], dtype=float) + total_params_tmp = sum(v.size for v in base_params.values()) + num_mcmc = total_params_tmp // num_walkers + 2 + + fake_w_L_data = np.ones((num_mcmc, num_walkers)) + rng = np.random.default_rng(42) + fake_e_L_data = rng.standard_normal((num_mcmc, num_walkers)) * 0.1 + + # Single-slot holder for the live params dict. We can't key by ``id(wf)`` + # because ``MCMC.hamiltonian_data`` setter calls ``apply_diff_mask`` which + # rewraps the wavefunction with a fresh instance every time it's reassigned + # (i.e. at the end of every optimization iteration). The single-slot + # approach assumes one MCMC instance is alive at a time inside this test. + params_holder: dict[str, dict[str, np.ndarray] | None] = {"params": None} + + def register_params(_wf, params): + params_holder["params"] = params + + def lookup_params(_wf): + return params_holder["params"] + + # Counter that varies the fake O matrix per get_dln_WF call, so successive + # SR systems differ and CG warm-start has actual work to do. Reset between + # the two run_once invocations so both branches see identical input streams. + call_idx = {"count": 0} + + def fake_get_variational_blocks( + self, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=True, + opt_JNN_param=True, + opt_lambda_param=False, + opt_J3_basis_exp=False, + opt_J3_basis_coeff=False, + opt_lambda_basis_exp=False, + opt_lambda_basis_coeff=False, + ): + blocks = [] + pos = lookup_params(self) + if opt_J1_param and "j1_param" in pos: + arr = pos["j1_param"] + blocks.append(VariationalParameterBlock(name="j1_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_J2_param and "j2_param" in pos: + arr = pos["j2_param"] + blocks.append(VariationalParameterBlock(name="j2_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_lambda_param and "lambda_matrix" in pos: + arr = pos["lambda_matrix"] + blocks.append(VariationalParameterBlock(name="lambda_matrix", values=arr, shape=arr.shape, size=int(arr.size))) + return blocks + + def fake_apply_block_updates(self, blocks, thetas, learning_rate): + params = lookup_params(self) + idx = 0 + for block in blocks: + blk_slice = thetas[idx : idx + block.size] + idx += block.size + if blk_slice.size == 0: + continue + delta = blk_slice.reshape(block.shape) + params[block.name] = params[block.name] + learning_rate * delta + return self + + def fake_run(self, num_mcmc_steps: int = 0, max_time=None): + return None + + def fake_get_dln_WF( + self, + blocks, + num_mcmc_warmup_steps=0, + chosen_param_index=None, + lambda_projectors=None, + num_orb_projection=None, + ): + call_idx["count"] += 1 + total = sum(block.size for block in blocks) + rng_local = np.random.default_rng(123 + call_idx["count"]) + return rng_local.standard_normal((num_mcmc, self.num_walkers, total)) * 0.01 + + def fake_get_E(self, num_mcmc_warmup_steps: int = 0, num_mcmc_bin_blocks: int = 1): + return (0.0, 0.0, 0.0, 0.0) + + def fake_get_gF( + self, + num_mcmc_warmup_steps, + num_mcmc_bin_blocks, + blocks, + lambda_projectors=None, + num_orb_projection=None, + chosen_param_index=None, + ): + total = sum(block.size for block in blocks) + return np.ones(total, dtype=float), np.ones(total, dtype=float) + + monkeypatch.setattr(Wavefunction_data, "get_variational_blocks", fake_get_variational_blocks, raising=False) + monkeypatch.setattr(Wavefunction_data, "apply_block_updates", fake_apply_block_updates, raising=False) + monkeypatch.setattr(MCMC, "run", fake_run, raising=False) + monkeypatch.setattr(MCMC, "get_E", fake_get_E, raising=False) + monkeypatch.setattr(MCMC, "get_gF", fake_get_gF, raising=False) + monkeypatch.setattr(MCMC, "get_dln_WF", fake_get_dln_WF, raising=False) + monkeypatch.setattr(MCMC, "w_L", property(lambda self: fake_w_L_data), raising=False) + monkeypatch.setattr(MCMC, "e_L", property(lambda self: fake_e_L_data), raising=False) + + def run_once(use_device_collectives: bool): + call_idx["count"] = 0 # reset so both branches see the same per-iter inputs + mcmc = MCMC( + hamiltonian_data=hamiltonian_data, + Dt=Dt, + mcmc_seed=mcmc_seed, + epsilon_AS=epsilon_AS, + num_walkers=num_walkers, + comput_position_deriv=False, + comput_log_WF_param_deriv=True, + comput_e_L_param_deriv=False, + random_discretized_mesh=True, + ) + params = {k: v.copy() for k, v in base_params.items()} + register_params(mcmc.hamiltonian_data.wavefunction_data, params) + mcmc.run_optimize( + num_mcmc_steps=num_mcmc, + num_opt_steps=num_opt_steps, + num_mcmc_warmup_steps=0, + num_mcmc_bin_blocks=1, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=False, + opt_JNN_param=False, + opt_lambda_param=True, + optimizer_kwargs={ + "method": "sr", + "delta": 1.0e-3, + "epsilon": 1.0e-3, + "cg_flag": True, + "cg_max_iter": 200, + "cg_tol": 1.0e-12, + }, + use_device_collectives=use_device_collectives, + ) + return params + + cpu_params = run_once(use_device_collectives=False) + dev_params = run_once(use_device_collectives=True) + + for key in cpu_params: + cpu_v = cpu_params[key] + dev_v = dev_params[key] + # Sanity: 3 iters of warm-started CG produced a non-trivial param trail. + assert not np.array_equal(cpu_v, base_params[key]), f"baseline CPU update is trivial for {key}" + np.testing.assert_allclose( + dev_v, + cpu_v, + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=f"device vs CPU CG warm-start mismatch for {key} (regime={regime})", + ) + + jax.clear_caches() + + @pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvtz_cart.h5"]) def test_opt_with_projected_MOs(trexio_file, monkeypatch): """After run_optimize with opt_with_projected_MOs=True the final wavefunction From c7a07e46b5323cc1cc20b2bfaf337e9532d5ff53 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 8 May 2026 21:49:29 +0900 Subject: [PATCH 61/97] Auto-select use_device_collectives in CLI by JAX backend (GPU=True, else=False) --- jqmc/jqmc_cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jqmc/jqmc_cli.py b/jqmc/jqmc_cli.py index d752b4cc..2c043831 100644 --- a/jqmc/jqmc_cli.py +++ b/jqmc/jqmc_cli.py @@ -496,6 +496,10 @@ def _cli(): logger.info("=" * num_sep_line) logger.info("Printing out information in hamitonian_data instance.") mcmc.hamiltonian_data._logger_info() + # Pick the SR backend per JAX device: GPU favours the on-device + # (jax.shard_map + NCCL) path, CPU favours the legacy mpi4py + SciPy + # path. Anything else falls back to the CPU path. + use_device_collectives = jax.default_backend() == "gpu" mcmc.run_optimize( num_mcmc_steps=num_mcmc_steps, num_opt_steps=num_opt_steps, @@ -514,6 +518,7 @@ def _cli(): opt_lambda_basis_coeff=opt_lambda_basis_coeff, max_time=max_time, optimizer_kwargs=optimizer_kwargs, + use_device_collectives=use_device_collectives, ) logger.info("") From 4c74ac85d3d425d56f049b4d246b854ec57b2f10 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 8 May 2026 22:46:03 +0900 Subject: [PATCH 62/97] Update tests. One of them exceeds 6 hours. --- .github/workflows/jqmc-run-full-pytest.yml | 15 ++++----------- .github/workflows/jqmc-run-long-pytest.yml | 4 ++-- .github/workflows/jqmc-run-rc-pytest.yml | 5 +++++ .github/workflows/jqmc-run-short-pytest.yml | 4 ++-- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index 187207bd..91d27792 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -47,7 +47,7 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - - name: Test jqmc FP64 (intra-software comparisons) + - name: Test jqmc FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append @@ -67,7 +67,7 @@ jobs: pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - name: Test jqmc FP32+FP64 (intra-software comparisons) + - name: Test jqmc FP32+FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed @@ -87,18 +87,11 @@ jobs: pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - - name: Test jqmc FP64 (inter-software comparisons) + - name: Test jqmc FP64 (Inter-software comparisons) run: | pytest -s -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - name: Test jqmc FP32+FP64 (QMC kernels without MPI) - run: | - pytest -s -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - - name: Test jqmc FP64 (QMC kernels without MPI) run: | pytest -s -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append @@ -106,7 +99,7 @@ jobs: pytest -s -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - name: Test jqmc-tool (toolset for jqmc) + - name: Test jqmc-tool (Toolset for jqmc) run: | pytest -s -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append diff --git a/.github/workflows/jqmc-run-long-pytest.yml b/.github/workflows/jqmc-run-long-pytest.yml index 89f4e3db..99248a8a 100644 --- a/.github/workflows/jqmc-run-long-pytest.yml +++ b/.github/workflows/jqmc-run-long-pytest.yml @@ -47,7 +47,7 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - - name: Test jqmc FP64/FP32+FP64 (intra-software comparisons) + - name: Test jqmc FP64/FP32+FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py pytest -s -v tests/test_init_electron_configurations.py @@ -71,7 +71,7 @@ jobs: pytest -s -v tests/test_ao_basis_optimization.py pytest -s -v tests/test_mixed_precision.py --precision-mode=mixed - - name: Test jqmc FP64 (inter-software comparisons) + - name: Test jqmc FP64 (Inter-software comparisons) run: | pytest -s -v tests/test_comparison_with_turborvb_ECP.py pytest -s -v tests/test_comparison_with_turborvb_AE.py diff --git a/.github/workflows/jqmc-run-rc-pytest.yml b/.github/workflows/jqmc-run-rc-pytest.yml index 95f8b433..97165f1c 100644 --- a/.github/workflows/jqmc-run-rc-pytest.yml +++ b/.github/workflows/jqmc-run-rc-pytest.yml @@ -51,6 +51,11 @@ jobs: run: | pytest -s -v tests/test_jqmc_command_lines.py + - name: Test jqmc FP64 (Inter-software comparisons) + run: | + pytest -s -v tests/test_comparison_with_turborvb_ECP.py + pytest -s -v tests/test_comparison_with_turborvb_AE.py + - name: Test jqmc FP64 (QMC kernels without MPI, FP64) run: | pytest -s -v tests/test_jqmc_mcmc.py diff --git a/.github/workflows/jqmc-run-short-pytest.yml b/.github/workflows/jqmc-run-short-pytest.yml index 2e40103f..55877b9d 100644 --- a/.github/workflows/jqmc-run-short-pytest.yml +++ b/.github/workflows/jqmc-run-short-pytest.yml @@ -58,7 +58,7 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - - name: Test jqmc FP64 (intra-software comparisons) + - name: Test jqmc FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --skip-heavy pytest -s -v tests/test_init_electron_configurations.py --skip-heavy @@ -74,7 +74,7 @@ jobs: pytest -s -v tests/test_lrdmc_force.py --skip-heavy pytest -s -v tests/test_mixed_precision.py --precision-mode=mixed --skip-heavy - - name: Test jqmc FP32+FP64 (intra-software comparisons) + - name: Test jqmc FP32+FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --skip-heavy --precision-mode=mixed pytest -s -v tests/test_init_electron_configurations.py --skip-heavy --precision-mode=mixed From 285f43975732a634cea0735c846ec0997463ad37 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sat, 9 May 2026 23:58:30 +0900 Subject: [PATCH 63/97] Improve test_jqmc_mcmc.py::test_sr_lm_device_matches_cpu --- tests/test_jqmc_mcmc.py | 115 ++++++++++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 28 deletions(-) diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index dd4fe155..3bf5f890 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -1460,7 +1460,7 @@ def run_once(use_device_collectives: bool): (2, True, 200, 4), ], ) -def test_sr_lm_device_matches_cpu(lm_subspace_dim, cg_flag, num_mcmc, num_walkers): +def test_sr_lm_device_matches_cpu(lm_subspace_dim, cg_flag, num_mcmc, num_walkers, monkeypatch): """LM / aSR end-to-end optimization with ``use_device_collectives`` toggled. @@ -1472,16 +1472,29 @@ def test_sr_lm_device_matches_cpu(lm_subspace_dim, cg_flag, num_mcmc, num_walker - ``lm_subspace_dim = 0``: aSR (gamma from H_0/H_1/H_2/S_2) - ``lm_subspace_dim = N`` (positive small): subspace LM (top-N + SR collective) - Both modes are well-conditioned at the chosen sample sizes: - ``solve_linear_method``'s eigenvalue / argmax operations have - unique well-separated winners, so the chain SR theta -> LM matrices -> - eigvec selection is Lipschitz. Strict consistency tolerance applies. - - ``lm_subspace_dim = -1`` (full-space LM) is intentionally not tested: - the augmented H_bar matrix always has many near-degenerate eigenvalues - (from gauge freedoms / redundant parameters) so the LM solver is - non-Lipschitz to round-off in the SR theta. Full-space LM is rarely - used in practice; subspace LM and aSR cover the supported workflows. + What is compared, and why: + - aSR (``lm_subspace_dim = 0``): gamma scaling is a smooth function + of the SR direction, so the final wf parameters depend + continuously on ``theta_SR`` and can be compared at strict + tolerance. + - Subspace LM (``lm_subspace_dim != 0``): ``solve_linear_method`` + contains two argmax operations (dgelscut parameter elimination + and ``argmax(|v_0|^2)`` eigenvector selection) that are + discontinuous in their inputs -- a round-off-level perturbation + can flip the selected mode and produce O(1e-3) jumps in the + downstream output (final wf parameters, ``E_lm``, etc.) even + though both branches run deterministically. Note in particular + that ``E_lm = eigvals_lm[argmax(|v_0|^2)]`` is *not* a + continuous function of ``H_bar``: the ranking by ``|v_0|^2`` + is unrelated to the eigenvalue ordering, so an argmax flip can + jump ``E_lm`` by the gap between two arbitrary eigenvalues. + Instead, compare the inputs to ``solve_linear_method`` + (``H_0, f_vec, S, K, B``) -- these depend continuously on + ``theta_SR``, so they are the right boundary at which to + verify that the device-branch SR solve agrees with the CPU + branch. Whatever ``solve_linear_method`` does downstream + (including any argmax flips) is shared CPU code and not part + of what this test is meant to cover. Single optimization step only: ``num_opt_steps > 1`` would diverge the MCMC trajectories once round-off-level wf differences accumulate. @@ -1531,7 +1544,27 @@ def build_mcmc(): comput_e_L_param_deriv=True, # required by use_lm=True ) + # Pristine reference, captured before any monkeypatching so chained + # spies (one per ``run_once`` call) all delegate to the real solver. + orig_solve_linear_method = MCMC.solve_linear_method + def run_once(use_device_collectives: bool): + lm_inputs: list[dict] = [] + + def spy(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon): + lm_inputs.append( + { + "H_0": float(H_0), + "f_vec": np.asarray(f_vec).copy(), + "S": np.asarray(S_matrix).copy(), + "K": np.asarray(K_matrix).copy(), + "B": np.asarray(B_matrix).copy(), + } + ) + return orig_solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon) + + monkeypatch.setattr(MCMC, "solve_linear_method", staticmethod(spy)) + mcmc = build_mcmc() mcmc.run_optimize( num_mcmc_steps=num_mcmc, @@ -1561,28 +1594,54 @@ def run_once(use_device_collectives: bool): use_device_collectives=use_device_collectives, ) wf = mcmc.hamiltonian_data.wavefunction_data - captured: dict[str, np.ndarray] = {} + wf_params: dict[str, np.ndarray] = {} if wf.jastrow_data.jastrow_one_body_data is not None: - captured["j1_param"] = np.asarray(wf.jastrow_data.jastrow_one_body_data.jastrow_1b_param) + wf_params["j1_param"] = np.asarray(wf.jastrow_data.jastrow_one_body_data.jastrow_1b_param) if wf.jastrow_data.jastrow_two_body_data is not None: - captured["j2_param"] = np.asarray(wf.jastrow_data.jastrow_two_body_data.jastrow_2b_param) + wf_params["j2_param"] = np.asarray(wf.jastrow_data.jastrow_two_body_data.jastrow_2b_param) if wf.jastrow_data.jastrow_three_body_data is not None: - captured["j3_matrix"] = np.asarray(wf.jastrow_data.jastrow_three_body_data.j_matrix) + wf_params["j3_matrix"] = np.asarray(wf.jastrow_data.jastrow_three_body_data.j_matrix) if wf.geminal_data is not None: - captured["lambda_matrix"] = np.asarray(wf.geminal_data.lambda_matrix) - return captured - - cpu_params = run_once(use_device_collectives=False) - dev_params = run_once(use_device_collectives=True) - - for key in cpu_params: - np.testing.assert_allclose( - dev_params[key], - cpu_params[key], - atol=atol_consistency, - rtol=rtol_consistency, - err_msg=(f"device vs CPU LM mismatch for {key} (lm_subspace_dim={lm_subspace_dim}, cg_flag={cg_flag})"), + wf_params["lambda_matrix"] = np.asarray(wf.geminal_data.lambda_matrix) + return wf_params, lm_inputs + + cpu_params, cpu_lm_inputs = run_once(use_device_collectives=False) + dev_params, dev_lm_inputs = run_once(use_device_collectives=True) + + if lm_subspace_dim == 0: + # aSR path: solve_linear_method is not invoked; final wf params are + # Lipschitz in theta_SR via gamma scaling. + assert cpu_lm_inputs == [] and dev_lm_inputs == [] + for key in cpu_params: + np.testing.assert_allclose( + dev_params[key], + cpu_params[key], + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=(f"device vs CPU aSR mismatch for {key} (lm_subspace_dim={lm_subspace_dim}, cg_flag={cg_flag})"), + ) + else: + # Subspace LM: compare only the inputs to solve_linear_method. + # These depend continuously on theta_SR, so they are the natural + # boundary at which the device-branch SR solve can be verified + # against the CPU branch. Anything past this point (E_lm, c_vec, + # final wf params) goes through argmax operations inside + # solve_linear_method and is not safe to compare strictly. + assert len(cpu_lm_inputs) == len(dev_lm_inputs) > 0, ( + f"solve_linear_method was not invoked (cpu={len(cpu_lm_inputs)}, dev={len(dev_lm_inputs)})" ) + for step, (c_in, d_in) in enumerate(zip(cpu_lm_inputs, dev_lm_inputs)): + for key in ("H_0", "f_vec", "S", "K", "B"): + np.testing.assert_allclose( + d_in[key], + c_in[key], + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=( + f"device vs CPU LM-input mismatch for {key} at step {step} " + f"(lm_subspace_dim={lm_subspace_dim}, cg_flag={cg_flag})" + ), + ) jax.clear_caches() From 36aaa74816d398733350eb41ad45def28d94f980 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sun, 10 May 2026 08:27:00 +0900 Subject: [PATCH 64/97] Run VMC optimize on multi-GPUs (#71) --- .github/workflows/jqmc-run-full-pytest.yml | 15 +- .github/workflows/jqmc-run-long-pytest.yml | 4 +- .github/workflows/jqmc-run-rc-pytest.yml | 5 + .github/workflows/jqmc-run-short-pytest.yml | 4 +- jqmc/_jqmc_utility.py | 37 + jqmc/jqmc_cli.py | 13 +- jqmc/jqmc_gfmc.py | 6 +- jqmc/jqmc_mcmc.py | 901 ++++++++++++++----- tests/conftest.py | 32 + tests/test_jqmc_mcmc.py | 942 ++++++++++++++++++++ 10 files changed, 1728 insertions(+), 231 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index 187207bd..91d27792 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -47,7 +47,7 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - - name: Test jqmc FP64 (intra-software comparisons) + - name: Test jqmc FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append @@ -67,7 +67,7 @@ jobs: pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - name: Test jqmc FP32+FP64 (intra-software comparisons) + - name: Test jqmc FP32+FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed @@ -87,18 +87,11 @@ jobs: pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - - name: Test jqmc FP64 (inter-software comparisons) + - name: Test jqmc FP64 (Inter-software comparisons) run: | pytest -s -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - name: Test jqmc FP32+FP64 (QMC kernels without MPI) - run: | - pytest -s -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - - name: Test jqmc FP64 (QMC kernels without MPI) run: | pytest -s -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append @@ -106,7 +99,7 @@ jobs: pytest -s -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - name: Test jqmc-tool (toolset for jqmc) + - name: Test jqmc-tool (Toolset for jqmc) run: | pytest -s -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append diff --git a/.github/workflows/jqmc-run-long-pytest.yml b/.github/workflows/jqmc-run-long-pytest.yml index 89f4e3db..99248a8a 100644 --- a/.github/workflows/jqmc-run-long-pytest.yml +++ b/.github/workflows/jqmc-run-long-pytest.yml @@ -47,7 +47,7 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - - name: Test jqmc FP64/FP32+FP64 (intra-software comparisons) + - name: Test jqmc FP64/FP32+FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py pytest -s -v tests/test_init_electron_configurations.py @@ -71,7 +71,7 @@ jobs: pytest -s -v tests/test_ao_basis_optimization.py pytest -s -v tests/test_mixed_precision.py --precision-mode=mixed - - name: Test jqmc FP64 (inter-software comparisons) + - name: Test jqmc FP64 (Inter-software comparisons) run: | pytest -s -v tests/test_comparison_with_turborvb_ECP.py pytest -s -v tests/test_comparison_with_turborvb_AE.py diff --git a/.github/workflows/jqmc-run-rc-pytest.yml b/.github/workflows/jqmc-run-rc-pytest.yml index 95f8b433..97165f1c 100644 --- a/.github/workflows/jqmc-run-rc-pytest.yml +++ b/.github/workflows/jqmc-run-rc-pytest.yml @@ -51,6 +51,11 @@ jobs: run: | pytest -s -v tests/test_jqmc_command_lines.py + - name: Test jqmc FP64 (Inter-software comparisons) + run: | + pytest -s -v tests/test_comparison_with_turborvb_ECP.py + pytest -s -v tests/test_comparison_with_turborvb_AE.py + - name: Test jqmc FP64 (QMC kernels without MPI, FP64) run: | pytest -s -v tests/test_jqmc_mcmc.py diff --git a/.github/workflows/jqmc-run-short-pytest.yml b/.github/workflows/jqmc-run-short-pytest.yml index 2e40103f..55877b9d 100644 --- a/.github/workflows/jqmc-run-short-pytest.yml +++ b/.github/workflows/jqmc-run-short-pytest.yml @@ -58,7 +58,7 @@ jobs: python -m pip install flake8 pytest pytest-cov python -m pip install . - - name: Test jqmc FP64 (intra-software comparisons) + - name: Test jqmc FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --skip-heavy pytest -s -v tests/test_init_electron_configurations.py --skip-heavy @@ -74,7 +74,7 @@ jobs: pytest -s -v tests/test_lrdmc_force.py --skip-heavy pytest -s -v tests/test_mixed_precision.py --precision-mode=mixed --skip-heavy - - name: Test jqmc FP32+FP64 (intra-software comparisons) + - name: Test jqmc FP32+FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --skip-heavy --precision-mode=mixed pytest -s -v tests/test_init_electron_configurations.py --skip-heavy --precision-mode=mixed diff --git a/jqmc/_jqmc_utility.py b/jqmc/_jqmc_utility.py index da207123..55493162 100644 --- a/jqmc/_jqmc_utility.py +++ b/jqmc/_jqmc_utility.py @@ -41,8 +41,10 @@ from functools import cache from logging import getLogger +import jax import numpy as np import numpy.typing as npt +from mpi4py import MPI # set logger logger = getLogger("jqmc").getChild(__name__) @@ -51,6 +53,41 @@ num_sep_line = 66 +def check_mpi4py_jax_distribution_consistency() -> None: + """Raise ``RuntimeError`` if ``jax.process_count()`` differs from MPI world size. + + Required precondition for any production code path that relies on + ``jax.shard_map`` + ``psum`` / ``all_gather`` to aggregate across MPI + ranks (currently the device branch of ``MCMC.run_optimize``; future + GFMC device migrations will share this requirement). Without + ``jax.distributed.initialize(cluster_detection_method="mpi4py")``, each + rank's JAX sees only its own local devices and the cross-rank + collectives silently degenerate to per-rank-local computation, producing + wrong results that don't match the global solution. + + Called at the entry point of every public driver loop (``MCMC.run``, + ``MCMC.run_optimize``, ``GFMC_t.run``, ``GFMC_n.run``) so the failure + surfaces immediately rather than after substantial work has been done + on stale per-rank state. + + The CLI (``jqmc_cli.py``) initializes JAX distributed automatically. + User scripts that import ``MCMC`` / ``GFMC`` directly must call + ``jax.distributed.initialize(cluster_detection_method="mpi4py")`` + themselves before invoking these driver loops (see ``jqmc_cli.py`` + for the standard recipe, including the proxy-strip workaround). + """ + mpi_size = MPI.COMM_WORLD.Get_size() + if mpi_size != jax.process_count(): + raise RuntimeError( + f"MPI/JAX rank-count mismatch: mpi_size={mpi_size} vs " + f"jax.process_count()={jax.process_count()}. JAX cross-rank " + "collectives (psum / all_gather) require ``jax.distributed.initialize(" + "cluster_detection_method='mpi4py')`` to be called before any " + "production driver loop. The CLI does this automatically; user " + "scripts importing MCMC / GFMC must call it themselves." + ) + + def _generate_init_electron_configurations( tot_num_electron_up: int, tot_num_electron_dn: int, diff --git a/jqmc/jqmc_cli.py b/jqmc/jqmc_cli.py index f19d569a..2c043831 100644 --- a/jqmc/jqmc_cli.py +++ b/jqmc/jqmc_cli.py @@ -48,7 +48,7 @@ # jQMC from ._header_footer import _print_footer, _print_header -from ._jqmc_utility import num_sep_line +from ._jqmc_utility import check_mpi4py_jax_distribution_consistency, num_sep_line from ._precision import configure as configure_precision from ._precision import mode_label as precision_mode_label from ._precision import zone_detail as precision_zone_detail @@ -233,6 +233,12 @@ def _cli(): logger.info("") jax_distributed_is_initialized = False + # Surface silently-failed distributed init: ``initialize`` may swallow + # exceptions (the try/except above) but if we are actually under + # ``mpirun -n N>=2`` and JAX still sees only 1 process, every downstream + # device-collective call would silently produce per-rank-local results. + check_mpi4py_jax_distribution_consistency() + if jax_distributed_is_initialized: # global JAX device global_device_info = jax.devices() @@ -490,6 +496,10 @@ def _cli(): logger.info("=" * num_sep_line) logger.info("Printing out information in hamitonian_data instance.") mcmc.hamiltonian_data._logger_info() + # Pick the SR backend per JAX device: GPU favours the on-device + # (jax.shard_map + NCCL) path, CPU favours the legacy mpi4py + SciPy + # path. Anything else falls back to the CPU path. + use_device_collectives = jax.default_backend() == "gpu" mcmc.run_optimize( num_mcmc_steps=num_mcmc_steps, num_opt_steps=num_opt_steps, @@ -508,6 +518,7 @@ def _cli(): opt_lambda_basis_coeff=opt_lambda_basis_coeff, max_time=max_time, optimizer_kwargs=optimizer_kwargs, + use_device_collectives=use_device_collectives, ) logger.info("") diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 18da7468..ab86d865 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -55,7 +55,7 @@ from mpi4py import MPI from ._diff_mask import DiffMask, apply_diff_mask -from ._jqmc_utility import _generate_init_electron_configurations +from ._jqmc_utility import _generate_init_electron_configurations, check_mpi4py_jax_distribution_consistency from ._precision import get_tolerance_min from ._setting import ( GFMC_MIN_BIN_BLOCKS, @@ -651,6 +651,8 @@ def run(self, num_mcmc_steps: int = 50, max_time: int = 86400) -> None: num_branching (int): number of branching (reconfiguration of walkers). max_time (int): maximum time in sec. """ + check_mpi4py_jax_distribution_consistency() + # set timer timer_projection_init = 0.0 timer_projection_total = 0.0 @@ -4690,6 +4692,8 @@ def run(self, num_mcmc_steps: int = 50, max_time: int = 86400) -> None: num_branching (int): number of branching (reconfiguration of walkers). max_time (int): maximum time in sec. """ + check_mpi4py_jax_distribution_consistency() + # initialize numpy random seed np.random.seed(self.__mpi_seed) diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index e0eca0f3..5e8ad004 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -58,7 +58,7 @@ from mpi4py import MPI from ._diff_mask import DiffMask, apply_diff_mask -from ._jqmc_utility import _generate_init_electron_configurations +from ._jqmc_utility import _generate_init_electron_configurations, check_mpi4py_jax_distribution_consistency from ._setting import ( MCMC_MIN_BIN_BLOCKS, MCMC_MIN_WARMUP_STEPS, @@ -121,6 +121,222 @@ def _loglevel_devel(self, message, *args, **kwargs): mpi_size = mpi_comm.Get_size() +# --------------------------------------------------------------------------- +# Device-resident SR solvers (JAX-native, sharding + psum / all_gather) +# +# Implements the four SR solve paths from ``optimizer_on_gpu.md``: +# - wide + direct : (X X^T + eps I) y = X F via psum +# - wide + CG : same system, conjugate gradient with psum'd matvec +# - tall + direct : (X^T X + eps I) z = F, theta = X z via all_gather +# - tall + CG : same system, conjugate gradient on replicated inputs +# +# Compiled kernels are cached at module level so the JIT cost is paid once +# per process. The same code path runs on: +# - single-process CPU (psum is trivial; mesh has 1 device) +# - multi-process CPU + Gloo (after ``jax.distributed.initialize``) +# - GPU (single or multi process; psum maps to NVLink / NCCL) +# --------------------------------------------------------------------------- +def _get_sr_mesh(): + """Lazy-built 1-D ``Mesh`` over all visible JAX devices, axis name 'rank'.""" + cached = getattr(_get_sr_mesh, "_cached", None) + if cached is not None: + return cached + from jax.sharding import Mesh + + mesh = Mesh(np.array(jax.devices()), axis_names=("rank",)) + _get_sr_mesh._cached = mesh + return mesh + + +def _cg_while_loop(b, apply_A, x0, max_iter, tol, dtype): + """Plain CG with break-on-breakdown, packed into ``jax.lax.while_loop``. + + Returns ``(x, sqrt(rs), num_iter)`` matching the NumPy reference's + return signature. ``apply_A`` may close over ``psum`` collectives so + this helper is only safe to call from inside a ``shard_map``. + """ + tiny = jnp.asarray(jnp.finfo(dtype).tiny, dtype=dtype) + tol_sq = tol * tol + + r0 = b - apply_A(x0) + rs0 = jnp.dot(r0, r0) + state0 = ( + x0, + r0, + r0, # p + rs0, + jnp.int32(0), + jnp.bool_(False), # breakdown + ) + + def cond(state): + _x, _r, _p, rs, k, breakdown = state + return (k < max_iter) & (rs > tol_sq) & jnp.logical_not(breakdown) + + def body(state): + x, r, p, rs_old, k, breakdown = state + Ap = apply_A(p) + denom = jnp.dot(p, Ap) + new_breakdown = breakdown | jnp.logical_not(jnp.isfinite(denom)) | (jnp.abs(denom) <= tiny) + safe_denom = jnp.where(new_breakdown, jnp.asarray(1.0, dtype=dtype), denom) + alpha = rs_old / safe_denom + x_new = jnp.where(new_breakdown, x, x + alpha * p) + r_new = jnp.where(new_breakdown, r, r - alpha * Ap) + rs_new_real = jnp.dot(r_new, r_new) + rs_new = jnp.where(new_breakdown, rs_old, rs_new_real) + safe_rs_old = jnp.where(rs_old > 0, rs_old, jnp.asarray(1.0, dtype=dtype)) + beta = rs_new / safe_rs_old + p_new = jnp.where(new_breakdown, p, r_new + beta * p) + return (x_new, r_new, p_new, rs_new, k + 1, new_breakdown) + + x_f, _r_f, _p_f, rs_f, k_f, _bk_f = jax.lax.while_loop(cond, body, state0) + return x_f, jnp.sqrt(rs_f), k_f + + +def _get_sr_wide_direct_kernel(): + """Wide-matrix direct SR solve: ``theta = (X X^T + eps I)^{-1} (X F)``. + + Inputs sharded along sample axis 'rank'. Output replicated on every rank. + """ + cached = getattr(_get_sr_wide_direct_kernel, "_cached", None) + if cached is not None: + return cached + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + + @partial( + jax.shard_map, + mesh=mesh, + in_specs=(PSpec(None, "rank"), PSpec("rank"), PSpec()), + out_specs=PSpec(), + ) + def _solve(X, F, epsilon): + XXT_local = X @ X.T + XF_local = X @ F + XXT = jax.lax.psum(XXT_local, "rank") + XF = jax.lax.psum(XF_local, "rank") + XXT = XXT + epsilon * jnp.eye(XXT.shape[0], dtype=XXT.dtype) + return jnp.linalg.solve(XXT, XF) + + _get_sr_wide_direct_kernel._cached = _solve + return _solve + + +def _get_sr_wide_cg_kernel(): + """Wide-matrix CG SR solve. Returns ``(theta, sqrt(rs), num_iter)``.""" + cached = getattr(_get_sr_wide_cg_kernel, "_cached", None) + if cached is not None: + return cached + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + + @partial( + jax.shard_map, + mesh=mesh, + in_specs=( + PSpec(None, "rank"), # X (P, N_local) + PSpec("rank"), # F (N_local,) + PSpec(), # epsilon scalar + PSpec(), # max_iter scalar + PSpec(), # tol scalar + PSpec(), # x0 (P,) replicated + ), + out_specs=(PSpec(), PSpec(), PSpec()), + ) + def _solve(X, F, epsilon, max_iter, tol, x0): + # b = psum(X F) + b = jax.lax.psum(X @ F, "rank") # (P,) + + def apply_A(v): + # v replicated across ranks; X.T @ v is local; X @ ... is local. + local = X @ (X.T @ v) + return jax.lax.psum(local, "rank") + epsilon * v + + return _cg_while_loop(b, apply_A, x0, max_iter, tol, X.dtype) + + _get_sr_wide_cg_kernel._cached = _solve + return _solve + + +def _get_sr_tall_direct_kernel(): + """Tall-matrix direct SR solve via push-through identity. + + Solves ``(X^T X + eps I) y = F`` (smaller system in sample space) and + returns ``theta = X y`` replicated across ranks. + """ + cached = getattr(_get_sr_tall_direct_kernel, "_cached", None) + if cached is not None: + return cached + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + + @partial( + jax.shard_map, + mesh=mesh, + in_specs=(PSpec(None, "rank"), PSpec("rank"), PSpec()), + out_specs=PSpec(), + # all_gather produces values that are replicated by construction but + # JAX cannot statically prove this on a 1-axis mesh; disable the check. + check_vma=False, + ) + def _solve(X, F, epsilon): + # Gather all sample columns onto every rank. In the tall regime + # ``num_samples_total`` is small by construction, so the replicated + # ``(P, N_total)`` matrix is affordable; the solve over ``N_total`` + # is the cheap dimension. + X_full = jax.lax.all_gather(X, "rank", axis=1, tiled=True) # (P, N_total) + F_full = jax.lax.all_gather(F, "rank", tiled=True) # (N_total,) + XTX = X_full.T @ X_full + XTX = XTX + epsilon * jnp.eye(XTX.shape[0], dtype=XTX.dtype) + y = jnp.linalg.solve(XTX, F_full) + return X_full @ y + + _get_sr_tall_direct_kernel._cached = _solve + return _solve + + +def _get_sr_tall_cg_kernel(): + """Tall-matrix CG SR solve via push-through identity.""" + cached = getattr(_get_sr_tall_cg_kernel, "_cached", None) + if cached is not None: + return cached + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + + @partial( + jax.shard_map, + mesh=mesh, + in_specs=( + PSpec(None, "rank"), # X (P, N_local) + PSpec("rank"), # F (N_local,) + PSpec(), # epsilon + PSpec(), # max_iter + PSpec(), # tol + PSpec(), # x0 (N_total,) replicated + ), + out_specs=(PSpec(), PSpec(), PSpec(), PSpec()), + check_vma=False, # all_gather output is replicated but not statically inferrable + ) + def _solve(X, F, epsilon, max_iter, tol, x0): + X_full = jax.lax.all_gather(X, "rank", axis=1, tiled=True) + F_full = jax.lax.all_gather(F, "rank", tiled=True) + + def apply_A(v): + return X_full.T @ (X_full @ v) + epsilon * v + + y, residual, num_iter = _cg_while_loop(F_full, apply_A, x0, max_iter, tol, X.dtype) + # Return y (sample-space CG solution) too so the caller can persist + # it as a warm-start for the next optimization step. + return X_full @ y, y, residual, num_iter + + _get_sr_tall_cg_kernel._cached = _solve + return _solve + + class MCMC: """Production VMC/MCMC driver with multiple walkers. @@ -461,6 +677,8 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: - Accumulates energies, weights, forces, and wavefunction gradients into public buffers (``w_L``, ``e_L``, ``dln_Psi_*`` etc.). - Logs timing statistics and acceptance ratios at the end of the run. """ + check_mpi4py_jax_distribution_consistency() + # timer_counter timer_mcmc_total = 0.0 timer_mcmc_update_init = 0.0 @@ -2405,6 +2623,131 @@ def solve_linear_method( return c_vec, E_lm + @staticmethod + def _shard_X_F(X_local: npt.NDArray, F_local: npt.NDArray): + """Convert host-local NumPy ``(X, F)`` into shard_map-ready ``jax.Array`` s. + + ``X_local`` is sharded along axis 1 (samples), ``F_local`` along axis 0. + Single-process: returns plain ``jnp.array`` (1-rank mesh = identity). + Multi-process: stitches host-local slices into a global ``jax.Array``. + """ + X_jnp = jnp.asarray(X_local) + F_jnp = jnp.asarray(F_local) + if jax.process_count() <= 1: + return X_jnp, F_jnp + + from jax.sharding import NamedSharding + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + X_global = jax.make_array_from_process_local_data(NamedSharding(mesh, PSpec(None, "rank")), X_jnp) + F_global = jax.make_array_from_process_local_data(NamedSharding(mesh, PSpec("rank")), F_jnp) + return X_global, F_global + + @staticmethod + def _replicated_jax_array(arr: npt.NDArray): + """Wrap a host-local NumPy array as a replicated ``jax.Array`` across ranks. + + For multi-process runs the ``arr`` must be identical on every rank (e.g. + a CG warm-start state that was psum'd before being persisted). + """ + jnp_arr = jnp.asarray(arr) + if jax.process_count() <= 1: + return jnp_arr + + from jax.sharding import NamedSharding + from jax.sharding import PartitionSpec as PSpec + + mesh = _get_sr_mesh() + return jax.make_array_from_process_local_data(NamedSharding(mesh, PSpec()), jnp_arr) + + def _sr_solve_wide_direct_device( + self, + X_local: npt.NDArray, + F_local: npt.NDArray, + epsilon: float, + ) -> npt.NDArray: + """Wide-matrix direct SR solve via shard_map + psum + ``jnp.linalg.solve``. + + Args: + X_local: Local (diag_S-normalized) design matrix ``(P, N_local)``. + F_local: Local right-hand-side ``(N_local,)``. + epsilon: Tikhonov regularization scalar. + + Returns: + ``theta = (X X^T + eps I)^{-1} (X F)`` of shape ``(P,)``, + identical on every rank. + """ + solver = _get_sr_wide_direct_kernel() + X_g, F_g = self._shard_X_F(X_local, F_local) + eps_jnp = jnp.asarray(epsilon, dtype=X_g.dtype) + theta = solver(X_g, F_g, eps_jnp) + theta.block_until_ready() + return np.asarray(theta) + + def _sr_solve_wide_cg_device( + self, + X_local: npt.NDArray, + F_local: npt.NDArray, + epsilon: float, + max_iter: int, + tol: float, + x0: npt.NDArray, + ) -> tuple[npt.NDArray, float, int]: + """Wide-matrix CG SR solve. Returns ``(theta, residual, num_iter)``. + + ``x0`` must be ``(P,)`` and identical on every rank (warm-start carried + across optimization iterations). + """ + solver = _get_sr_wide_cg_kernel() + X_g, F_g = self._shard_X_F(X_local, F_local) + x0_g = self._replicated_jax_array(np.asarray(x0, dtype=X_local.dtype)) + eps_jnp = jnp.asarray(epsilon, dtype=X_g.dtype) + max_iter_jnp = jnp.asarray(int(max_iter), dtype=jnp.int32) + tol_jnp = jnp.asarray(tol, dtype=X_g.dtype) + theta, residual, num_iter = solver(X_g, F_g, eps_jnp, max_iter_jnp, tol_jnp, x0_g) + theta.block_until_ready() + return np.asarray(theta), float(residual), int(num_iter) + + def _sr_solve_tall_direct_device( + self, + X_local: npt.NDArray, + F_local: npt.NDArray, + epsilon: float, + ) -> npt.NDArray: + """Tall-matrix direct SR solve (push-through identity).""" + solver = _get_sr_tall_direct_kernel() + X_g, F_g = self._shard_X_F(X_local, F_local) + eps_jnp = jnp.asarray(epsilon, dtype=X_g.dtype) + theta = solver(X_g, F_g, eps_jnp) + theta.block_until_ready() + return np.asarray(theta) + + def _sr_solve_tall_cg_device( + self, + X_local: npt.NDArray, + F_local: npt.NDArray, + epsilon: float, + max_iter: int, + tol: float, + x0: npt.NDArray, + ) -> tuple[npt.NDArray, npt.NDArray, float, int]: + """Tall-matrix CG SR solve. Returns ``(theta, y, residual, num_iter)``. + + ``x0`` and ``y`` live in the sample space, shape ``(N_total,)``; + ``y`` is the CG solution (suitable as warm-start next iteration). + ``theta = X y`` lives in parameter space, shape ``(P,)``. + """ + solver = _get_sr_tall_cg_kernel() + X_g, F_g = self._shard_X_F(X_local, F_local) + x0_g = self._replicated_jax_array(np.asarray(x0, dtype=X_local.dtype)) + eps_jnp = jnp.asarray(epsilon, dtype=X_g.dtype) + max_iter_jnp = jnp.asarray(int(max_iter), dtype=jnp.int32) + tol_jnp = jnp.asarray(tol, dtype=X_g.dtype) + theta, y, residual, num_iter = solver(X_g, F_g, eps_jnp, max_iter_jnp, tol_jnp, x0_g) + theta.block_until_ready() + return np.asarray(theta), np.asarray(y), float(residual), int(num_iter) + def run_optimize( self, num_mcmc_steps: int = 100, @@ -2424,6 +2767,7 @@ def run_optimize( opt_lambda_basis_exp: bool = False, opt_lambda_basis_coeff: bool = False, optimizer_kwargs: dict | None = None, + use_device_collectives: bool = True, ): """Optimize wavefunction parameters using SR or optax. @@ -2457,6 +2801,21 @@ def run_optimize( ``use_lm=True`` enables LM with keys (``lm_subspace_dim``, ``lm_cond``); other ``method`` names are optax constructors (e.g., ``"adam"``) and receive remaining keys. + use_device_collectives (bool, optional): If True (default), run + the SR cross-rank reductions and linear solve via + ``jax.shard_map`` + ``jax.lax.psum`` / ``all_gather`` (NCCL on + multi-process GPU, Gloo on multi-process CPU, trivial on + single-process). If False, fall back to ``mpi_comm.Reduce`` / + ``Allreduce`` / ``Alltoallv`` + ``scipy.linalg.solve`` (legacy + mpi4py + SciPy path). + + When True under ``mpirun -n N>=2``, ``jax.distributed.initialize( + cluster_detection_method="mpi4py")`` must have been called + before ``run_optimize``; otherwise the device path silently + produces per-rank-local results that don't match the global + solution. ``run_optimize`` raises ``RuntimeError`` if it + detects ``mpi_size != jax.process_count()`` to surface this + misconfiguration. Notes: - Persists optax optimizer state across calls when method and hyperparameters match. @@ -2501,6 +2860,8 @@ def run_optimize( if not self.__comput_e_L_param_deriv: raise RuntimeError("use_lm requires comput_e_L_param_deriv=True.") + check_mpi4py_jax_distribution_consistency() + optax_kwargs = { k: v for k, v in optimizer_kwargs.items() @@ -3101,6 +3462,17 @@ def _conjugate_gradient_numpy( logger.info(f"The number of total samples is {num_samples_total}.") logger.info(f"SR matrix dimension: {num_params} x {num_params}.") + # Announce which SR solve path will be used for this iteration. + _sr_regime = "wide" if num_params < num_samples_total else "tall" + _sr_method = "CG" if sr_cg_flag else "direct" + _sr_backend = "device (jax.shard_map + psum/all_gather)" if use_device_collectives else "CPU (mpi4py + scipy)" + logger.debug( + "SR solver path: regime=%s, method=%s, backend=%s", + _sr_regime, + _sr_method, + _sr_backend, + ) + # make the SR matrix scale-invariant (i.e., normalize) ## compute X_w@X.T diag_S_local = np.einsum("jk,kj->j", X_local, X_local.T) @@ -3156,240 +3528,341 @@ def _conjugate_gradient_numpy( logger.debug("X is a wide matrix. Proceed w/o the push-through identity.") logger.debug("theta = (S+epsilon*I)^{-1}*f = (X * X^T + epsilon*I)^{-1} * X F...") if not sr_cg_flag: - logger.info("Using the direct solver for the inverse of S.") - logger.debug( - f"Estimated X_local @ X_local.T.bytes per MPI = {X_local.shape[0] ** 2 * X_local.dtype.itemsize / (2**30)} gib." - ) - # compute local sum of X * X^T - X_X_T_local = X_local @ X_local.T - logger.devel(f"X_X_T_local.shape = {X_X_T_local.shape}.") - # compute global sum of X * X^T - if mpi_rank == 0: - X_X_T = np.empty(X_X_T_local.shape, dtype=dtype_mcmc_np) - else: - X_X_T = None - mpi_comm.Reduce(X_X_T_local, X_X_T, op=MPI.SUM, root=0) - # compute local sum of X @ F - X_F_local = X_local @ F_local # shape (num_param, ) - logger.devel(f"X_F_local.shape = {X_F_local.shape}.") - # compute global sum of X @ F - if mpi_rank == 0: - X_F = np.empty(X_F_local.shape, dtype=dtype_mcmc_np) - else: - X_F = None - mpi_comm.Reduce(X_F_local, X_F, op=MPI.SUM, root=0) - # compute theta - if mpi_rank == 0: - logger.devel(f"X @ X.T.shape = {X_X_T.shape}.") - logger.devel(f"X @ F.shape = {X_F.shape}.") - # (X X^T + eps*I) x = X F ->solve-> x = (X X^T + eps*I)^{-1} X F - X_X_T[np.diag_indices_from(X_X_T)] += epsilon - - X_X_T_inv_X_F = scipy.linalg.solve(X_X_T, X_F, assume_a="sym") - # theta = (X_w X^T + eps*I)^{-1} X_w F - theta_all = X_X_T_inv_X_F + if use_device_collectives: + logger.info("Using the direct solver for the inverse of S (device-resident, shard_map + psum).") + logger.debug( + f"Estimated X_local @ X_local.T.bytes per MPI = {X_local.shape[0] ** 2 * X_local.dtype.itemsize / (2**30)} gib." + ) + theta_all = self._sr_solve_wide_direct_device( + X_local=X_local, + F_local=F_local, + epsilon=epsilon, + ) + logger.devel(f"[device] theta_all (w/o the push through identity) = {theta_all}.") + logger.devel( + f"[device] theta_all (w/o the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) else: - theta_all = None - # Broadcast theta_all to all ranks - theta_all = mpi_comm.bcast(theta_all, root=0) - logger.devel(f"[new] theta_all (w/o the push through identity) = {theta_all}.") - logger.devel( - f"[new] theta_all (w/o the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." - ) + logger.info("Using the direct solver for the inverse of S.") + logger.debug( + f"Estimated X_local @ X_local.T.bytes per MPI = {X_local.shape[0] ** 2 * X_local.dtype.itemsize / (2**30)} gib." + ) + # compute local sum of X * X^T + X_X_T_local = X_local @ X_local.T + logger.devel(f"X_X_T_local.shape = {X_X_T_local.shape}.") + # compute global sum of X * X^T + if mpi_rank == 0: + X_X_T = np.empty(X_X_T_local.shape, dtype=dtype_mcmc_np) + else: + X_X_T = None + mpi_comm.Reduce(X_X_T_local, X_X_T, op=MPI.SUM, root=0) + # compute local sum of X @ F + X_F_local = X_local @ F_local # shape (num_param, ) + logger.devel(f"X_F_local.shape = {X_F_local.shape}.") + # compute global sum of X @ F + if mpi_rank == 0: + X_F = np.empty(X_F_local.shape, dtype=dtype_mcmc_np) + else: + X_F = None + mpi_comm.Reduce(X_F_local, X_F, op=MPI.SUM, root=0) + # compute theta + if mpi_rank == 0: + logger.devel(f"X @ X.T.shape = {X_X_T.shape}.") + logger.devel(f"X @ F.shape = {X_F.shape}.") + # (X X^T + eps*I) x = X F ->solve-> x = (X X^T + eps*I)^{-1} X F + X_X_T[np.diag_indices_from(X_X_T)] += epsilon + + X_X_T_inv_X_F = scipy.linalg.solve(X_X_T, X_F, assume_a="sym") + # theta = (X_w X^T + eps*I)^{-1} X_w F + theta_all = X_X_T_inv_X_F + else: + theta_all = None + # Broadcast theta_all to all ranks + theta_all = mpi_comm.bcast(theta_all, root=0) + logger.devel(f"[new] theta_all (w/o the push through identity) = {theta_all}.") + logger.devel( + f"[new] theta_all (w/o the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) else: - logger.info("Using conjugate gradient for the inverse of S.") - logger.info(f" [CG] threshold {sr_cg_tol}.") - logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") - # conjugate gradient solver - # Compute b = X @ F (distributed) - X_F_local = X_local @ F_local # shape (num_param, ) - X_F = np.zeros_like(X_F_local) - mpi_comm.Allreduce(X_F_local, X_F, op=MPI.SUM) - - def apply_S_primal_numpy(v): - XTv_local = X_local.T @ v - XXTv_local = X_local @ XTv_local - XXTv_global = np.empty_like(XXTv_local) - mpi_comm.Allreduce(XXTv_local, XXTv_global, op=MPI.SUM) - return XXTv_global + epsilon * v - - if sr_cg_warm_start_primal is not None and sr_cg_warm_start_primal.shape == X_F.shape: - x0 = sr_cg_warm_start_primal + if use_device_collectives: + logger.info("Using conjugate gradient for the inverse of S (device-resident, shard_map + psum).") + logger.info(f" [CG] threshold {sr_cg_tol}.") + logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") + num_params_local = X_local.shape[0] + if sr_cg_warm_start_primal is not None and sr_cg_warm_start_primal.shape == (num_params_local,): + x0 = sr_cg_warm_start_primal + else: + x0 = np.zeros(num_params_local, dtype=dtype_mcmc_np) + theta_all, final_residual, num_steps = self._sr_solve_wide_cg_device( + X_local=X_local, + F_local=F_local, + epsilon=epsilon, + max_iter=sr_cg_max_iter, + tol=sr_cg_tol, + x0=x0, + ) + sr_cg_warm_start_primal = np.array(theta_all, copy=True) + logger.devel(f" [CG] Final residual: {final_residual:.3e}") + logger.info(f" [CG] Converged in {num_steps} steps") + if num_steps == sr_cg_max_iter: + logger.info(" [CG] Conjugate gradient did not converge!!") + logger.devel(f"[device/cg] theta_all (w/o the push through identity) = {theta_all}.") + logger.devel(f"[device/cg] theta_all: min, max = {np.min(theta_all)}, {np.max(theta_all)}.") else: - x0 = np.zeros_like(X_F) - - theta_all, final_residual, num_steps = _conjugate_gradient_numpy( - np.asarray(X_F, dtype=dtype_mcmc_np), - apply_S_primal_numpy, - np.asarray(x0, dtype=dtype_mcmc_np), - sr_cg_max_iter, - sr_cg_tol, - ) - sr_cg_warm_start_primal = np.array(theta_all, copy=True) - logger.devel(f" [CG] Final residual: {final_residual:.3e}") - logger.info(f" [CG] Converged in {num_steps} steps") - if num_steps == sr_cg_max_iter: - logger.info(" [CG] Conjugate gradient did not converge!!") - logger.devel(f"[new/cg] theta_all (w/o the push through identity) = {theta_all}.") - logger.devel( - f"[new/cg] theta_all (w/o the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." - ) + logger.info("Using conjugate gradient for the inverse of S.") + logger.info(f" [CG] threshold {sr_cg_tol}.") + logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") + # conjugate gradient solver + # Compute b = X @ F (distributed) + X_F_local = X_local @ F_local # shape (num_param, ) + X_F = np.zeros_like(X_F_local) + mpi_comm.Allreduce(X_F_local, X_F, op=MPI.SUM) + + def apply_S_primal_numpy(v): + XTv_local = X_local.T @ v + XXTv_local = X_local @ XTv_local + XXTv_global = np.empty_like(XXTv_local) + mpi_comm.Allreduce(XXTv_local, XXTv_global, op=MPI.SUM) + return XXTv_global + epsilon * v + + if sr_cg_warm_start_primal is not None and sr_cg_warm_start_primal.shape == X_F.shape: + x0 = sr_cg_warm_start_primal + else: + x0 = np.zeros_like(X_F) + + theta_all, final_residual, num_steps = _conjugate_gradient_numpy( + np.asarray(X_F, dtype=dtype_mcmc_np), + apply_S_primal_numpy, + np.asarray(x0, dtype=dtype_mcmc_np), + sr_cg_max_iter, + sr_cg_tol, + ) + sr_cg_warm_start_primal = np.array(theta_all, copy=True) + logger.devel(f" [CG] Final residual: {final_residual:.3e}") + logger.info(f" [CG] Converged in {num_steps} steps") + if num_steps == sr_cg_max_iter: + logger.info(" [CG] Conjugate gradient did not converge!!") + logger.devel(f"[new/cg] theta_all (w/o the push through identity) = {theta_all}.") + logger.devel( + f"[new/cg] theta_all (w/o the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) else: # num_params >= num_samples: # if True: logger.debug("X is a tall matrix. Proceed w/ the push-through identity.") logger.debug("theta = (S+epsilon*I)^{-1}*f = X(X^T * X + epsilon*I)^{-1} * F...") - # Get local shapes - N, M = X_local.shape - P = mpi_size # number of ranks - - # Compute how many rows each rank should own (distribute the remainder) - counts = [N // P + (1 if i < (N % P) else 0) for i in range(P)] - - # Compute starting row index for each rank in the original array - displs = [sum(counts[:i]) for i in range(P)] - N_local = counts[mpi_rank] # number of rows this rank will receive - - # Build send buffers by slicing X and Xw into P row-chunks - # Each chunk is flattened so we can send in one go. - sendbuf_X = np.concatenate([X_local[displs[i] : displs[i] + counts[i], :].ravel() for i in range(P)]) - - # Prepare sendcounts and displacements in units of elements - sendcounts = [counts[i] * M for i in range(P)] - sdispls = [sum(sendcounts[:i]) for i in range(P)] + if use_device_collectives: + # Device path: shard_map + all_gather handles redistribution + # internally; no Alltoallv prep on host. + if not sr_cg_flag: + logger.info( + "Using the direct solver for the inverse of S " + "(device-resident, shard_map + all_gather, push-through identity)." + ) + theta_all = self._sr_solve_tall_direct_device( + X_local=X_local, + F_local=F_local, + epsilon=epsilon, + ) + logger.devel( + f"[device] theta_all (w/ the push through identity): " + f"min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) + else: + logger.info( + "Using conjugate gradient for the inverse of S " + "(device-resident, shard_map + all_gather, push-through identity)." + ) + logger.info(f" [CG] threshold {sr_cg_tol}.") + logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") + num_samples_total_local = int(F_local.shape[0]) * mpi_size + if sr_cg_warm_start_dual is not None and sr_cg_warm_start_dual.shape == (num_samples_total_local,): + x0 = sr_cg_warm_start_dual + else: + x0 = np.zeros(num_samples_total_local, dtype=dtype_mcmc_np) + theta_all, y_sample, final_residual, num_steps = self._sr_solve_tall_cg_device( + X_local=X_local, + F_local=F_local, + epsilon=epsilon, + max_iter=sr_cg_max_iter, + tol=sr_cg_tol, + x0=x0, + ) + # Persist sample-space CG solution as warm-start for the + # next opt iteration (matches CPU branch's behavior). + sr_cg_warm_start_dual = np.array(y_sample, copy=True) + logger.devel(f" [CG] Final residual: {final_residual:.3e}") + logger.info(f" [CG] Converged in {num_steps} steps") + if num_steps == sr_cg_max_iter: + logger.info(" [CG] Conjugate gradient did not converge!!") + logger.devel( + f"[device/cg] theta_all (w/ the push through identity): " + f"min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) + # Skip the rest of the legacy CPU tall block. + # Fall through to the shared scale-back below. + # (Sentinel handled by Python control flow: the CPU branch's + # remaining code lives inside the same `else:` arm; we mirror + # by jumping past it via early-continuation pattern below.) + _device_tall_done = True + else: + _device_tall_done = False - # Prepare recvcounts and displacements: - # each rank will receive 'counts[mpi_rank]*M' elements from each of the P ranks - recvcounts = [counts[mpi_rank] * M] * P - rdispls = [i * counts[mpi_rank] * M for i in range(P)] + if _device_tall_done: + pass # device path produced theta_all already + else: + # Legacy CPU path: redistribute X via Alltoallv, then solve. + # Get local shapes + N, M = X_local.shape + P = mpi_size # number of ranks - # Allocate receive buffers - recvbuf_X = np.empty(sum(recvcounts), dtype=X_local.dtype) + # Compute how many rows each rank should own (distribute the remainder) + counts = [N // P + (1 if i < (N % P) else 0) for i in range(P)] - # Perform the all-to-all variable-sized exchange - mpi_comm.Alltoallv( - [sendbuf_X, sendcounts, sdispls, MPI.DOUBLE], [recvbuf_X, recvcounts, rdispls, MPI.DOUBLE] - ) + # Compute starting row index for each rank in the original array + displs = [sum(counts[:i]) for i in range(P)] + N_local = counts[mpi_rank] # number of rows this rank will receive - # Reshape the flat receive buffer into a 3D array - # shape = (P sources, N_local rows, M cols) - buf_X = recvbuf_X.reshape(P, N_local, M) + # Build send buffers by slicing X and Xw into P row-chunks + # Each chunk is flattened so we can send in one go. + sendbuf_X = np.concatenate([X_local[displs[i] : displs[i] + counts[i], :].ravel() for i in range(P)]) - # Rearrange into final 2D arrays of shape (N_local, M * P) - # by stacking each source's M columns side by side - X_re_local = np.hstack([buf_X[i] for i in range(P)]) # shape (num_param/P, num_mcmc * num_walker * P) - logger.devel(f"X_re_local.shape = {X_re_local.shape}.") + # Prepare sendcounts and displacements in units of elements + sendcounts = [counts[i] * M for i in range(P)] + sdispls = [sum(sendcounts[:i]) for i in range(P)] - if not sr_cg_flag: - logger.info("Using the direct solver for the inverse of S.") - logger.devel( - f"Estimated X_local.T @ X_local.bytes per MPI = {X_re_local.shape[1] ** 2 * X_re_local.dtype.itemsize / (2**30)} gib." - ) - # compute local sum of X^T * X - X_T_X_local = X_re_local.T @ X_re_local - logger.devel(f"X_T_X_local.shape = {X_T_X_local.shape}.") - # compute global sum of X^T * X - if mpi_rank == 0: - X_T_X = np.empty(X_T_X_local.shape, dtype=dtype_mcmc_np) - else: - X_T_X = None - mpi_comm.Reduce(X_T_X_local, X_T_X, op=MPI.SUM, root=0) - # gather F_local from all ranks (concatenation, not element-wise sum) - F_local_count = F_local.shape[0] - F_recvcounts = mpi_comm.gather(F_local_count, root=0) - if mpi_rank == 0: - F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] - F = np.empty(sum(F_recvcounts), dtype=dtype_mcmc_np) - else: - F_displs = None - F = None - mpi_comm.Gatherv( - [F_local, MPI.DOUBLE], - [F, (F_recvcounts, F_displs), MPI.DOUBLE] if mpi_rank == 0 else [F, None], - root=0, - ) - if mpi_rank == 0: - logger.devel(f"X_T_X.shape = {X_T_X.shape}.") - logger.devel(f"F.shape = {F.shape}.") - X_T_X[np.diag_indices_from(X_T_X)] += epsilon - # (X^T X_w + eps*I) x = F ->solve-> x = (X^T X_w + eps*I)^{-1} F - X_T_X_inv_F = scipy.linalg.solve(X_T_X, F, assume_a="sym") - K = X_T_X_inv_F.shape[0] // mpi_size - else: - X_T_X_inv_F = None - K = None - # Broadcast K to all ranks so they know how big each chunk is - K = mpi_comm.bcast(K, root=0) + # Prepare recvcounts and displacements: + # each rank will receive 'counts[mpi_rank]*M' elements from each of the P ranks + recvcounts = [counts[mpi_rank] * M] * P + rdispls = [i * counts[mpi_rank] * M for i in range(P)] - X_T_X_inv_F_local = np.empty(K, dtype=dtype_mcmc_np) + # Allocate receive buffers + recvbuf_X = np.empty(sum(recvcounts), dtype=X_local.dtype) - mpi_comm.Scatter( - [X_T_X_inv_F, MPI.DOUBLE], # send buffer (only significant on root) - X_T_X_inv_F_local, # receive buffer (on each rank) - root=0, - ) - # theta = X_w (X^T X_w + eps*I)^{-1} F - theta_all_local = X_local @ X_T_X_inv_F_local - theta_all = np.empty(theta_all_local.shape, dtype=dtype_mcmc_np) - mpi_comm.Allreduce(theta_all_local, theta_all, op=MPI.SUM) - logger.devel(f"[new] theta_all (w/ the push through identity) = {theta_all}.") - logger.devel( - f"[new] theta_all (w/ the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." - ) - else: - logger.info("Using conjugate gradient for the inverse of S.") - logger.info(f" [CG] threshold {sr_cg_tol}.") - logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") - - def apply_dual_S_numpy(v): - Xv_local = X_re_local @ v - XTXv_local = X_re_local.T @ Xv_local - XTXv_global = np.empty_like(XTXv_local) - mpi_comm.Allreduce(XTXv_local, XTXv_global, op=MPI.SUM) - return XTXv_global + epsilon * v - - # Gather F_local from all ranks (concatenation) to form F_total of length M*P - F_local_count = F_local.shape[0] - F_recvcounts = mpi_comm.allgather(F_local_count) - F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] - F_total = np.empty(sum(F_recvcounts), dtype=dtype_mcmc_np) - mpi_comm.Allgatherv( - [F_local, MPI.DOUBLE], - [F_total, (F_recvcounts, F_displs), MPI.DOUBLE], - ) - if sr_cg_warm_start_dual is not None and sr_cg_warm_start_dual.shape == F_total.shape: - x0 = sr_cg_warm_start_dual - else: - x0 = np.zeros_like(F_total) - x_sol, final_residual, num_steps = _conjugate_gradient_numpy( - F_total, - apply_dual_S_numpy, - np.asarray(x0, dtype=dtype_mcmc_np), - sr_cg_max_iter, - sr_cg_tol, + # Perform the all-to-all variable-sized exchange + mpi_comm.Alltoallv( + [sendbuf_X, sendcounts, sdispls, MPI.DOUBLE], [recvbuf_X, recvcounts, rdispls, MPI.DOUBLE] ) - sr_cg_warm_start_dual = np.array(x_sol, copy=True) - - # theta = X @ x_sol, evaluated locally over X_re_local (N_local rows) - theta_local = X_re_local @ x_sol # shape (N_local,) - theta_local = np.asarray(theta_local) - N_local = theta_local.shape[0] - recvcounts = mpi_comm.allgather(N_local) - displs = [sum(recvcounts[:i]) for i in range(mpi_comm.Get_size())] + # Reshape the flat receive buffer into a 3D array + # shape = (P sources, N_local rows, M cols) + buf_X = recvbuf_X.reshape(P, N_local, M) - theta_all = np.empty(sum(recvcounts), dtype=theta_local.dtype) - mpi_comm.Allgatherv([theta_local, MPI.DOUBLE], [theta_all, (recvcounts, displs), MPI.DOUBLE]) + # Rearrange into final 2D arrays of shape (N_local, M * P) + # by stacking each source's M columns side by side + X_re_local = np.hstack([buf_X[i] for i in range(P)]) # shape (num_param/P, num_mcmc * num_walker * P) + logger.devel(f"X_re_local.shape = {X_re_local.shape}.") - logger.devel(f" [CG] Final residual: {final_residual:.3e}") - logger.info(f" [CG] Converged in {num_steps} steps") - if num_steps == sr_cg_max_iter: - logger.logger(" [CG] Conjugate gradient did not converge!") - logger.devel(f"[new/cg] theta_all (w/o the push through identity) = {theta_all}.") - logger.devel( - f"[new/cg] theta_all (w/ the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." - ) + if not sr_cg_flag: + logger.info("Using the direct solver for the inverse of S.") + logger.devel( + f"Estimated X_local.T @ X_local.bytes per MPI = {X_re_local.shape[1] ** 2 * X_re_local.dtype.itemsize / (2**30)} gib." + ) + # compute local sum of X^T * X + X_T_X_local = X_re_local.T @ X_re_local + logger.devel(f"X_T_X_local.shape = {X_T_X_local.shape}.") + # compute global sum of X^T * X + if mpi_rank == 0: + X_T_X = np.empty(X_T_X_local.shape, dtype=dtype_mcmc_np) + else: + X_T_X = None + mpi_comm.Reduce(X_T_X_local, X_T_X, op=MPI.SUM, root=0) + # gather F_local from all ranks (concatenation, not element-wise sum) + F_local_count = F_local.shape[0] + F_recvcounts = mpi_comm.gather(F_local_count, root=0) + if mpi_rank == 0: + F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] + F = np.empty(sum(F_recvcounts), dtype=dtype_mcmc_np) + else: + F_displs = None + F = None + mpi_comm.Gatherv( + [F_local, MPI.DOUBLE], + [F, (F_recvcounts, F_displs), MPI.DOUBLE] if mpi_rank == 0 else [F, None], + root=0, + ) + if mpi_rank == 0: + logger.devel(f"X_T_X.shape = {X_T_X.shape}.") + logger.devel(f"F.shape = {F.shape}.") + X_T_X[np.diag_indices_from(X_T_X)] += epsilon + # (X^T X_w + eps*I) x = F ->solve-> x = (X^T X_w + eps*I)^{-1} F + X_T_X_inv_F = scipy.linalg.solve(X_T_X, F, assume_a="sym") + K = X_T_X_inv_F.shape[0] // mpi_size + else: + X_T_X_inv_F = None + K = None + # Broadcast K to all ranks so they know how big each chunk is + K = mpi_comm.bcast(K, root=0) + + X_T_X_inv_F_local = np.empty(K, dtype=dtype_mcmc_np) + + mpi_comm.Scatter( + [X_T_X_inv_F, MPI.DOUBLE], # send buffer (only significant on root) + X_T_X_inv_F_local, # receive buffer (on each rank) + root=0, + ) + # theta = X_w (X^T X_w + eps*I)^{-1} F + theta_all_local = X_local @ X_T_X_inv_F_local + theta_all = np.empty(theta_all_local.shape, dtype=dtype_mcmc_np) + mpi_comm.Allreduce(theta_all_local, theta_all, op=MPI.SUM) + logger.devel(f"[new] theta_all (w/ the push through identity) = {theta_all}.") + logger.devel( + f"[new] theta_all (w/ the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) + else: + logger.info("Using conjugate gradient for the inverse of S.") + logger.info(f" [CG] threshold {sr_cg_tol}.") + logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") + + def apply_dual_S_numpy(v): + Xv_local = X_re_local @ v + XTXv_local = X_re_local.T @ Xv_local + XTXv_global = np.empty_like(XTXv_local) + mpi_comm.Allreduce(XTXv_local, XTXv_global, op=MPI.SUM) + return XTXv_global + epsilon * v + + # Gather F_local from all ranks (concatenation) to form F_total of length M*P + F_local_count = F_local.shape[0] + F_recvcounts = mpi_comm.allgather(F_local_count) + F_displs = [sum(F_recvcounts[:i]) for i in range(len(F_recvcounts))] + F_total = np.empty(sum(F_recvcounts), dtype=dtype_mcmc_np) + mpi_comm.Allgatherv( + [F_local, MPI.DOUBLE], + [F_total, (F_recvcounts, F_displs), MPI.DOUBLE], + ) + if sr_cg_warm_start_dual is not None and sr_cg_warm_start_dual.shape == F_total.shape: + x0 = sr_cg_warm_start_dual + else: + x0 = np.zeros_like(F_total) + x_sol, final_residual, num_steps = _conjugate_gradient_numpy( + F_total, + apply_dual_S_numpy, + np.asarray(x0, dtype=dtype_mcmc_np), + sr_cg_max_iter, + sr_cg_tol, + ) + sr_cg_warm_start_dual = np.array(x_sol, copy=True) + + # theta = X @ x_sol, evaluated locally over X_re_local (N_local rows) + theta_local = X_re_local @ x_sol # shape (N_local,) + theta_local = np.asarray(theta_local) + N_local = theta_local.shape[0] + + recvcounts = mpi_comm.allgather(N_local) + displs = [sum(recvcounts[:i]) for i in range(mpi_comm.Get_size())] + + theta_all = np.empty(sum(recvcounts), dtype=theta_local.dtype) + mpi_comm.Allgatherv([theta_local, MPI.DOUBLE], [theta_all, (recvcounts, displs), MPI.DOUBLE]) + + logger.devel(f" [CG] Final residual: {final_residual:.3e}") + logger.info(f" [CG] Converged in {num_steps} steps") + if num_steps == sr_cg_max_iter: + logger.logger(" [CG] Conjugate gradient did not converge!") + logger.devel(f"[new/cg] theta_all (w/o the push through identity) = {theta_all}.") + logger.devel( + f"[new/cg] theta_all (w/ the push through identity): min, max = {np.min(theta_all)}, {np.max(theta_all)}." + ) # theta, back to the original scale theta_all = theta_all / np.sqrt(diag_S) diff --git a/tests/conftest.py b/tests/conftest.py index 3ec4467f..d390b0d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,38 @@ import pytest +@pytest.fixture(scope="session", autouse=True) +def _jax_distributed_init(): + """Initialize ``jax.distributed`` once per pytest session under multi-rank MPI. + + The device branch of ``run_optimize`` (``use_device_collectives=True``) + relies on ``jax.lax.psum`` / ``jax.lax.all_gather`` to aggregate across + MPI ranks. Without ``jax.distributed.initialize`` each rank's JAX sees + only its own local devices (``jax.process_count() == 1``) and the + collectives degenerate to no-ops, silently producing wrong results. + + Mirrors the production CLI setup in ``jqmc_cli.py``: strip HTTP proxy + environment variables before calling ``initialize`` so the JAX gRPC + coordination service doesn't try to route the local-host connection + through a proxy (which is the typical cause of "hangs forever" on + macOS / corporate networks). + """ + import os + + from mpi4py import MPI + + if MPI.COMM_WORLD.Get_size() > 1: + # Same proxy-strip workaround as jqmc_cli.py. + for _proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(_proxy_var, None) + try: + jax.distributed.initialize(cluster_detection_method="mpi4py") + except Exception: + # already initialized in the same process, or backend cannot start + pass + yield + + def pytest_addoption(parser): """Add options for pytests.""" parser.addoption("--disable-jit", action="store_true", default=False, help="Disable jax.jit for pytests") diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index 5f8149ab..3bf5f890 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -48,6 +48,7 @@ sys.path.insert(0, project_root) from jqmc._precision import get_tolerance_min +from jqmc._setting import atol_consistency, rtol_consistency from jqmc.determinant import Geminal_data from jqmc.hamiltonians import Hamiltonian_data from jqmc.jastrow_factor import ( @@ -933,6 +934,947 @@ def fake_get_gF( jax.clear_caches() +@pytest.mark.parametrize( + "regime,cg_flag", + [ + ("wide", False), + ("wide", True), + ("tall", False), + ("tall", True), + ], +) +@pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvtz_cart.h5"]) +def test_sr_device_matches_cpu(trexio_file, regime, cg_flag, monkeypatch): + """Each of the four SR solve paths (wide/tall x direct/CG) must produce + the same parameter update on the JAX-native device branch as on the + legacy NumPy/SciPy/mpi4py CPU branch, given identical inputs. + + Single-process: ``psum`` is trivial and the device path reduces to its + local computation; this test verifies numerical agreement only. + Multi-rank validation requires actually running ``mpirun`` and is out of + scope for the unit suite. + """ + from mpi4py import MPI as _MPI + + if _MPI.COMM_WORLD.Get_size() != 1: + pytest.skip("Numerical-agreement test runs single-process only.") + + ( + structure_data, + _, + _, + _, + geminal_mo_data, + coulomb_potential_data, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), store_tuple=True + ) + + jastrow_onebody_data = Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=1.0, + structure_data=structure_data, + core_electrons=tuple([0] * len(structure_data.atomic_numbers)), + jastrow_1b_type="pade", + ) + jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="pade") + jastrow_data = Jastrow_data( + jastrow_one_body_data=jastrow_onebody_data, + jastrow_two_body_data=jastrow_twobody_data, + jastrow_three_body_data=None, + jastrow_nn_data=None, + ) + wavefunction_data = Wavefunction_data(jastrow_data=jastrow_data, geminal_data=geminal_mo_data) + hamiltonian_data = Hamiltonian_data( + structure_data=structure_data, + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + ) + + num_walkers = 2 + Dt = 2.0 + mcmc_seed = 12345 + epsilon_AS = 1.0e-6 + + # Build a parameter set sized for the requested regime. Single-process, + # so num_samples_total = num_mcmc * num_walkers. + base_params: dict[str, np.ndarray] = { + "j1_param": np.ones_like(np.array(jastrow_onebody_data.jastrow_1b_param)), + "j2_param": np.ones_like(np.array(jastrow_twobody_data.jastrow_2b_param)), + } + fixed_param_size = sum(v.size for v in base_params.values()) + + if regime == "tall": + num_mcmc = 1 + min_samples_total = num_mcmc * num_walkers + # Pad lambda_matrix so num_params >= num_samples_total. + lambda_size_needed = max(1, min_samples_total - fixed_param_size + 1) + base_params["lambda_matrix"] = np.ones(lambda_size_needed, dtype=float) + else: + base_params["lambda_matrix"] = np.array([[2.0, -2.0], [3.0, -3.0]], dtype=float) + total_params_tmp = sum(v.size for v in base_params.values()) + num_mcmc = total_params_tmp // num_walkers + 2 + + total_params = sum(v.size for v in base_params.values()) + num_samples_total = num_mcmc * num_walkers + if regime == "wide": + assert total_params < num_samples_total, f"wide setup invalid: {total_params} >= {num_samples_total}" + else: + assert total_params >= num_samples_total, f"tall setup invalid: {total_params} < {num_samples_total}" + + rng = np.random.default_rng(42) + fake_w_L_data = np.ones((num_mcmc, num_walkers)) + fake_e_L_data = rng.standard_normal((num_mcmc, num_walkers)) * 0.1 + + params_registry: dict[int, dict[str, np.ndarray]] = {} + + def register_params(wf, params): + params_registry[id(wf)] = params + + def lookup_params(wf): + return params_registry[id(wf)] + + def fake_get_variational_blocks( + self, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=True, + opt_JNN_param=True, + opt_lambda_param=False, + opt_J3_basis_exp=False, + opt_J3_basis_coeff=False, + opt_lambda_basis_exp=False, + opt_lambda_basis_coeff=False, + ): + blocks = [] + pos = lookup_params(self) + if opt_J1_param and "j1_param" in pos: + arr = pos["j1_param"] + blocks.append(VariationalParameterBlock(name="j1_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_J2_param and "j2_param" in pos: + arr = pos["j2_param"] + blocks.append(VariationalParameterBlock(name="j2_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_lambda_param and "lambda_matrix" in pos: + arr = pos["lambda_matrix"] + blocks.append(VariationalParameterBlock(name="lambda_matrix", values=arr, shape=arr.shape, size=int(arr.size))) + return blocks + + def fake_apply_block_updates(self, blocks, thetas, learning_rate): + params = lookup_params(self) + idx = 0 + for block in blocks: + blk_slice = thetas[idx : idx + block.size] + idx += block.size + if blk_slice.size == 0: + continue + delta = blk_slice.reshape(block.shape) + params[block.name] = params[block.name] + learning_rate * delta + return self + + def fake_run(self, num_mcmc_steps: int = 0, max_time=None): + return None + + # Deterministic O matrix so both runs see identical inputs. + def fake_get_dln_WF( + self, + blocks, + num_mcmc_warmup_steps=0, + chosen_param_index=None, + lambda_projectors=None, + num_orb_projection=None, + ): + total = sum(block.size for block in blocks) + rng_local = np.random.default_rng(123) + return rng_local.standard_normal((num_mcmc, self.num_walkers, total)) * 0.01 + + def fake_get_E(self, num_mcmc_warmup_steps: int = 0, num_mcmc_bin_blocks: int = 1): + return (0.0, 0.0, 0.0, 0.0) + + def fake_get_gF( + self, + num_mcmc_warmup_steps, + num_mcmc_bin_blocks, + blocks, + lambda_projectors=None, + num_orb_projection=None, + chosen_param_index=None, + ): + total = sum(block.size for block in blocks) + return np.ones(total, dtype=float), np.ones(total, dtype=float) + + monkeypatch.setattr(Wavefunction_data, "get_variational_blocks", fake_get_variational_blocks, raising=False) + monkeypatch.setattr(Wavefunction_data, "apply_block_updates", fake_apply_block_updates, raising=False) + monkeypatch.setattr(MCMC, "run", fake_run, raising=False) + monkeypatch.setattr(MCMC, "get_E", fake_get_E, raising=False) + monkeypatch.setattr(MCMC, "get_gF", fake_get_gF, raising=False) + monkeypatch.setattr(MCMC, "get_dln_WF", fake_get_dln_WF, raising=False) + monkeypatch.setattr(MCMC, "w_L", property(lambda self: fake_w_L_data), raising=False) + monkeypatch.setattr(MCMC, "e_L", property(lambda self: fake_e_L_data), raising=False) + + def run_once(use_device_collectives: bool): + mcmc = MCMC( + hamiltonian_data=hamiltonian_data, + Dt=Dt, + mcmc_seed=mcmc_seed, + epsilon_AS=epsilon_AS, + num_walkers=num_walkers, + comput_position_deriv=False, + comput_log_WF_param_deriv=True, + comput_e_L_param_deriv=False, + random_discretized_mesh=True, + ) + params = {k: v.copy() for k, v in base_params.items()} + register_params(mcmc.hamiltonian_data.wavefunction_data, params) + mcmc.run_optimize( + num_mcmc_steps=num_mcmc, + num_opt_steps=1, + num_mcmc_warmup_steps=0, + num_mcmc_bin_blocks=1, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=False, + opt_JNN_param=False, + opt_lambda_param=True, + optimizer_kwargs={ + "method": "sr", + "delta": 1.0e-3, + "epsilon": 1.0e-3, + "cg_flag": cg_flag, + "cg_max_iter": 200, + # CG iteration tolerance well below the project consistency + # tolerance, so the device-vs-CPU difference is dominated by + # float64 round-off, not CG residual. + "cg_tol": 1.0e-12, + }, + use_device_collectives=use_device_collectives, + ) + return params + + cpu_params = run_once(use_device_collectives=False) + dev_params = run_once(use_device_collectives=True) + + # Both branches do exactly the same float64 SR arithmetic, just with + # NumPy/SciPy/mpi4py vs JAX/shard_map; difference is round-off only. + # Use the project's strict-float64 consistency tolerance. + for key in cpu_params: + cpu_v = cpu_params[key] + dev_v = dev_params[key] + # Sanity: CPU branch produced a non-trivial update. + assert not np.array_equal(cpu_v, base_params[key]), f"baseline CPU update is trivial for {key}" + np.testing.assert_allclose( + dev_v, + cpu_v, + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=f"device vs CPU mismatch for {key} (regime={regime}, cg={cg_flag})", + ) + + jax.clear_caches() + + +@pytest.mark.parametrize( + "regime,cg_flag", + [ + ("wide", False), + ("wide", True), + ("tall", False), + ("tall", True), + ], +) +@pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvtz_cart.h5"]) +def test_sr_device_matches_cpu_multirank(trexio_file, regime, cg_flag, monkeypatch): + """Multi-rank counterpart to ``test_sr_device_matches_cpu``. + + Verifies that under ``mpirun -n N>=2``: + + - The legacy CPU branch (``mpi_comm.Reduce`` / ``Allreduce`` / ``Alltoallv`` + via mpi4py) and + - The device branch (``jax.lax.psum`` / ``all_gather`` via NCCL on GPU + or Gloo on CPU, dispatched through ``shard_map``) + + produce numerically equivalent ``theta`` updates for all four SR + paths (wide/tall x direct/CG). + + Each MPI rank is given *different* fake samples (via a rank-dependent + seed) so that the cross-rank reduction has actual work to do; if the + fixture-installed ``jax.distributed.initialize`` were missing, the + device branch would silently produce per-rank-local results that + wouldn't agree with the CPU branch's globally-aggregated result. + + Skipped on single-process runs (the single-process variant lives in + ``test_sr_device_matches_cpu``). + """ + from mpi4py import MPI as _MPI + + comm = _MPI.COMM_WORLD + mpi_size = comm.Get_size() + mpi_rank = comm.Get_rank() + + if mpi_size < 2: + pytest.skip("Multi-rank agreement test requires at least 2 MPI ranks.") + if jax.process_count() < 2: + pytest.skip( + "Multi-rank agreement test requires jax.distributed to be initialized " + "(JAX sees only 1 process despite multiple MPI ranks). The conftest " + "fixture should auto-init under ``mpirun -n N pytest``; check that the " + "init didn't silently fail (proxy env vars, network sandboxing)." + ) + + ( + structure_data, + _, + _, + _, + geminal_mo_data, + coulomb_potential_data, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), store_tuple=True + ) + + jastrow_onebody_data = Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=1.0, + structure_data=structure_data, + core_electrons=tuple([0] * len(structure_data.atomic_numbers)), + jastrow_1b_type="pade", + ) + jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="pade") + jastrow_data = Jastrow_data( + jastrow_one_body_data=jastrow_onebody_data, + jastrow_two_body_data=jastrow_twobody_data, + jastrow_three_body_data=None, + jastrow_nn_data=None, + ) + wavefunction_data = Wavefunction_data(jastrow_data=jastrow_data, geminal_data=geminal_mo_data) + hamiltonian_data = Hamiltonian_data( + structure_data=structure_data, + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + ) + + num_walkers = 2 + Dt = 2.0 + mcmc_seed = 12345 + epsilon_AS = 1.0e-6 + + # Build a parameter set sized for the requested regime; use mpi_size in + # the sample-count budget since the SR system sees all ranks' samples. + base_params: dict[str, np.ndarray] = { + "j1_param": np.ones_like(np.array(jastrow_onebody_data.jastrow_1b_param)), + "j2_param": np.ones_like(np.array(jastrow_twobody_data.jastrow_2b_param)), + } + fixed_param_size = sum(v.size for v in base_params.values()) + + if regime == "tall": + num_mcmc = 1 + min_samples_total = num_mcmc * num_walkers * mpi_size + lambda_size_needed = max(1, min_samples_total - fixed_param_size + 1) + base_params["lambda_matrix"] = np.ones(lambda_size_needed, dtype=float) + else: + base_params["lambda_matrix"] = np.array([[2.0, -2.0], [3.0, -3.0]], dtype=float) + total_params_tmp = sum(v.size for v in base_params.values()) + # Ensure num_mcmc * num_walkers * mpi_size > total_params_tmp. + num_mcmc = max(1, total_params_tmp // (num_walkers * mpi_size) + 2) + + total_params = sum(v.size for v in base_params.values()) + num_samples_total = num_mcmc * num_walkers * mpi_size + if regime == "wide": + assert total_params < num_samples_total, f"wide setup invalid: {total_params} >= {num_samples_total}" + else: + assert total_params >= num_samples_total, f"tall setup invalid: {total_params} < {num_samples_total}" + + # Rank-dependent fake data: each rank sees different samples so the + # cross-rank reduction is meaningful. Same seeds in both run_once calls + # so CPU and device branches see identical inputs. + fake_w_L_data = np.ones((num_mcmc, num_walkers)) + rng = np.random.default_rng(42 + mpi_rank) + fake_e_L_data = rng.standard_normal((num_mcmc, num_walkers)) * 0.1 + + params_holder: dict[str, dict[str, np.ndarray] | None] = {"params": None} + + def register_params(_wf, params): + params_holder["params"] = params + + def lookup_params(_wf): + return params_holder["params"] + + def fake_get_variational_blocks( + self, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=True, + opt_JNN_param=True, + opt_lambda_param=False, + opt_J3_basis_exp=False, + opt_J3_basis_coeff=False, + opt_lambda_basis_exp=False, + opt_lambda_basis_coeff=False, + ): + blocks = [] + pos = lookup_params(self) + if opt_J1_param and "j1_param" in pos: + arr = pos["j1_param"] + blocks.append(VariationalParameterBlock(name="j1_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_J2_param and "j2_param" in pos: + arr = pos["j2_param"] + blocks.append(VariationalParameterBlock(name="j2_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_lambda_param and "lambda_matrix" in pos: + arr = pos["lambda_matrix"] + blocks.append(VariationalParameterBlock(name="lambda_matrix", values=arr, shape=arr.shape, size=int(arr.size))) + return blocks + + def fake_apply_block_updates(self, blocks, thetas, learning_rate): + params = lookup_params(self) + idx = 0 + for block in blocks: + blk_slice = thetas[idx : idx + block.size] + idx += block.size + if blk_slice.size == 0: + continue + delta = blk_slice.reshape(block.shape) + params[block.name] = params[block.name] + learning_rate * delta + return self + + def fake_run(self, num_mcmc_steps: int = 0, max_time=None): + return None + + def fake_get_dln_WF( + self, + blocks, + num_mcmc_warmup_steps=0, + chosen_param_index=None, + lambda_projectors=None, + num_orb_projection=None, + ): + total = sum(block.size for block in blocks) + rng_local = np.random.default_rng(123 + mpi_rank) + return rng_local.standard_normal((num_mcmc, self.num_walkers, total)) * 0.01 + + def fake_get_E(self, num_mcmc_warmup_steps: int = 0, num_mcmc_bin_blocks: int = 1): + return (0.0, 0.0, 0.0, 0.0) + + def fake_get_gF( + self, + num_mcmc_warmup_steps, + num_mcmc_bin_blocks, + blocks, + lambda_projectors=None, + num_orb_projection=None, + chosen_param_index=None, + ): + total = sum(block.size for block in blocks) + return np.ones(total, dtype=float), np.ones(total, dtype=float) + + monkeypatch.setattr(Wavefunction_data, "get_variational_blocks", fake_get_variational_blocks, raising=False) + monkeypatch.setattr(Wavefunction_data, "apply_block_updates", fake_apply_block_updates, raising=False) + monkeypatch.setattr(MCMC, "run", fake_run, raising=False) + monkeypatch.setattr(MCMC, "get_E", fake_get_E, raising=False) + monkeypatch.setattr(MCMC, "get_gF", fake_get_gF, raising=False) + monkeypatch.setattr(MCMC, "get_dln_WF", fake_get_dln_WF, raising=False) + monkeypatch.setattr(MCMC, "w_L", property(lambda self: fake_w_L_data), raising=False) + monkeypatch.setattr(MCMC, "e_L", property(lambda self: fake_e_L_data), raising=False) + + def run_once(use_device_collectives: bool): + mcmc = MCMC( + hamiltonian_data=hamiltonian_data, + Dt=Dt, + mcmc_seed=mcmc_seed, + epsilon_AS=epsilon_AS, + num_walkers=num_walkers, + comput_position_deriv=False, + comput_log_WF_param_deriv=True, + comput_e_L_param_deriv=False, + random_discretized_mesh=True, + ) + params = {k: v.copy() for k, v in base_params.items()} + register_params(mcmc.hamiltonian_data.wavefunction_data, params) + mcmc.run_optimize( + num_mcmc_steps=num_mcmc, + num_opt_steps=1, + num_mcmc_warmup_steps=0, + num_mcmc_bin_blocks=1, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=False, + opt_JNN_param=False, + opt_lambda_param=True, + optimizer_kwargs={ + "method": "sr", + "delta": 1.0e-3, + "epsilon": 1.0e-3, + "cg_flag": cg_flag, + "cg_max_iter": 200, + "cg_tol": 1.0e-14, + }, + use_device_collectives=use_device_collectives, + ) + return params + + cpu_params = run_once(use_device_collectives=False) + dev_params = run_once(use_device_collectives=True) + + # Both branches must agree to consistency tolerance on every rank. + for key in cpu_params: + cpu_v = cpu_params[key] + dev_v = dev_params[key] + assert not np.array_equal(cpu_v, base_params[key]), f"baseline CPU update is trivial for {key} (rank={mpi_rank})" + np.testing.assert_allclose( + dev_v, + cpu_v, + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=(f"device vs CPU multirank mismatch for {key} (regime={regime}, cg={cg_flag}, rank={mpi_rank}/{mpi_size})"), + ) + + # Sanity: CPU branch's bcast / device branch's psum both replicate theta + # across ranks, so the wf updates should agree across ranks too. + rank0_cpu = comm.bcast({k: v.copy() for k, v in cpu_params.items()}, root=0) + for key in cpu_params: + np.testing.assert_allclose( + cpu_params[key], + rank0_cpu[key], + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=f"CPU branch theta differs across ranks for {key} (rank={mpi_rank})", + ) + rank0_dev = comm.bcast({k: v.copy() for k, v in dev_params.items()}, root=0) + for key in dev_params: + np.testing.assert_allclose( + dev_params[key], + rank0_dev[key], + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=f"device branch theta differs across ranks for {key} (rank={mpi_rank})", + ) + + jax.clear_caches() + + +@pytest.mark.parametrize( + "lm_subspace_dim,cg_flag,num_mcmc,num_walkers", + [ + # aSR (gamma scaling): smooth function, strict at any size. + (0, False, 10, 2), + (0, True, 10, 2), + # Subspace LM (size 2 + SR collective = 3 dims): well-conditioned + # once samples >> 3, so strict at 200 mcmc * 4 walkers = 800 samples. + (2, False, 200, 4), + (2, True, 200, 4), + ], +) +def test_sr_lm_device_matches_cpu(lm_subspace_dim, cg_flag, num_mcmc, num_walkers, monkeypatch): + """LM / aSR end-to-end optimization with ``use_device_collectives`` + toggled. + + The device branch only replaces the SR direct/CG solve; everything + downstream (``get_aH``, ``solve_linear_method``, aSR gamma) still runs + on the CPU/mpi4py path. + + Tested LM modes (cf. ``run_optimize`` ``optimizer_kwargs``): + - ``lm_subspace_dim = 0``: aSR (gamma from H_0/H_1/H_2/S_2) + - ``lm_subspace_dim = N`` (positive small): subspace LM (top-N + SR collective) + + What is compared, and why: + - aSR (``lm_subspace_dim = 0``): gamma scaling is a smooth function + of the SR direction, so the final wf parameters depend + continuously on ``theta_SR`` and can be compared at strict + tolerance. + - Subspace LM (``lm_subspace_dim != 0``): ``solve_linear_method`` + contains two argmax operations (dgelscut parameter elimination + and ``argmax(|v_0|^2)`` eigenvector selection) that are + discontinuous in their inputs -- a round-off-level perturbation + can flip the selected mode and produce O(1e-3) jumps in the + downstream output (final wf parameters, ``E_lm``, etc.) even + though both branches run deterministically. Note in particular + that ``E_lm = eigvals_lm[argmax(|v_0|^2)]`` is *not* a + continuous function of ``H_bar``: the ranking by ``|v_0|^2`` + is unrelated to the eigenvalue ordering, so an argmax flip can + jump ``E_lm`` by the gap between two arbitrary eigenvalues. + Instead, compare the inputs to ``solve_linear_method`` + (``H_0, f_vec, S, K, B``) -- these depend continuously on + ``theta_SR``, so they are the right boundary at which to + verify that the device-branch SR solve agrees with the CPU + branch. Whatever ``solve_linear_method`` does downstream + (including any argmax flips) is shared CPU code and not part + of what this test is meant to cover. + + Single optimization step only: ``num_opt_steps > 1`` would diverge the + MCMC trajectories once round-off-level wf differences accumulate. + """ + from mpi4py import MPI as _MPI + + if _MPI.COMM_WORLD.Get_size() != 1: + pytest.skip("Numerical-agreement test runs single-process only.") + + trexio_file_path = os.path.join(os.path.dirname(__file__), "trexio_example_files", "H2_ae_ccpvdz_cart.h5") + + def build_mcmc(): + ( + structure_data, + aos_data, + _, + _, + geminal_mo_data, + coulomb_potential_data, + ) = read_trexio_file(trexio_file=trexio_file_path, store_tuple=True) + + jastrow_data = Jastrow_data( + jastrow_one_body_data=Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=1.0, + structure_data=structure_data, + core_electrons=tuple([0] * len(structure_data.atomic_numbers)), + jastrow_1b_type="pade", + ), + jastrow_two_body_data=Jastrow_two_body_data.init_jastrow_two_body_data( + jastrow_2b_param=0.5, jastrow_2b_type="pade" + ), + jastrow_three_body_data=Jastrow_three_body_data.init_jastrow_three_body_data(orb_data=aos_data), + ) + wavefunction_data = Wavefunction_data(jastrow_data=jastrow_data, geminal_data=geminal_mo_data) + hamiltonian_data = Hamiltonian_data( + structure_data=structure_data, + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + ) + return MCMC( + hamiltonian_data=hamiltonian_data, + Dt=2.0, + mcmc_seed=12345, + num_walkers=num_walkers, + comput_position_deriv=False, + comput_log_WF_param_deriv=True, + comput_e_L_param_deriv=True, # required by use_lm=True + ) + + # Pristine reference, captured before any monkeypatching so chained + # spies (one per ``run_once`` call) all delegate to the real solver. + orig_solve_linear_method = MCMC.solve_linear_method + + def run_once(use_device_collectives: bool): + lm_inputs: list[dict] = [] + + def spy(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon): + lm_inputs.append( + { + "H_0": float(H_0), + "f_vec": np.asarray(f_vec).copy(), + "S": np.asarray(S_matrix).copy(), + "K": np.asarray(K_matrix).copy(), + "B": np.asarray(B_matrix).copy(), + } + ) + return orig_solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon) + + monkeypatch.setattr(MCMC, "solve_linear_method", staticmethod(spy)) + + mcmc = build_mcmc() + mcmc.run_optimize( + num_mcmc_steps=num_mcmc, + num_opt_steps=1, + num_mcmc_warmup_steps=0, + num_mcmc_bin_blocks=1, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=True, + opt_lambda_param=True, + optimizer_kwargs={ + "method": "sr", + "use_lm": True, + "lm_subspace_dim": lm_subspace_dim, + "lm_cond": 1.0e-3, + "delta": 0.1, + "epsilon": 1.0e-6, + "cg_flag": cg_flag, + # NB: cg_tol=1e-14 (near machine eps) is needed for the + # LM step to receive bit-comparable theta_SR from both + # branches. With cg_tol=1e-12, CG can early-terminate at + # mutually different points along the iteration trajectory, + # producing O(1e-3) differences that the LM step preserves. + "cg_max_iter": 2000, + "cg_tol": 1.0e-14, + }, + use_device_collectives=use_device_collectives, + ) + wf = mcmc.hamiltonian_data.wavefunction_data + wf_params: dict[str, np.ndarray] = {} + if wf.jastrow_data.jastrow_one_body_data is not None: + wf_params["j1_param"] = np.asarray(wf.jastrow_data.jastrow_one_body_data.jastrow_1b_param) + if wf.jastrow_data.jastrow_two_body_data is not None: + wf_params["j2_param"] = np.asarray(wf.jastrow_data.jastrow_two_body_data.jastrow_2b_param) + if wf.jastrow_data.jastrow_three_body_data is not None: + wf_params["j3_matrix"] = np.asarray(wf.jastrow_data.jastrow_three_body_data.j_matrix) + if wf.geminal_data is not None: + wf_params["lambda_matrix"] = np.asarray(wf.geminal_data.lambda_matrix) + return wf_params, lm_inputs + + cpu_params, cpu_lm_inputs = run_once(use_device_collectives=False) + dev_params, dev_lm_inputs = run_once(use_device_collectives=True) + + if lm_subspace_dim == 0: + # aSR path: solve_linear_method is not invoked; final wf params are + # Lipschitz in theta_SR via gamma scaling. + assert cpu_lm_inputs == [] and dev_lm_inputs == [] + for key in cpu_params: + np.testing.assert_allclose( + dev_params[key], + cpu_params[key], + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=(f"device vs CPU aSR mismatch for {key} (lm_subspace_dim={lm_subspace_dim}, cg_flag={cg_flag})"), + ) + else: + # Subspace LM: compare only the inputs to solve_linear_method. + # These depend continuously on theta_SR, so they are the natural + # boundary at which the device-branch SR solve can be verified + # against the CPU branch. Anything past this point (E_lm, c_vec, + # final wf params) goes through argmax operations inside + # solve_linear_method and is not safe to compare strictly. + assert len(cpu_lm_inputs) == len(dev_lm_inputs) > 0, ( + f"solve_linear_method was not invoked (cpu={len(cpu_lm_inputs)}, dev={len(dev_lm_inputs)})" + ) + for step, (c_in, d_in) in enumerate(zip(cpu_lm_inputs, dev_lm_inputs)): + for key in ("H_0", "f_vec", "S", "K", "B"): + np.testing.assert_allclose( + d_in[key], + c_in[key], + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=( + f"device vs CPU LM-input mismatch for {key} at step {step} " + f"(lm_subspace_dim={lm_subspace_dim}, cg_flag={cg_flag})" + ), + ) + + jax.clear_caches() + + +@pytest.mark.parametrize("regime", ["wide", "tall"]) +@pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvtz_cart.h5"]) +def test_sr_cg_warm_start_device_matches_cpu(trexio_file, regime, monkeypatch): + """Multi-step CG with warm-start: device branch must mirror CPU branch + after multiple optimization iterations. + + Each iteration the CG solver carries the previous step's solution as the + initial guess (``sr_cg_warm_start_primal`` for wide, ``sr_cg_warm_start_dual`` + for tall). Both CPU and device branches must persist this state correctly + so the final wf parameters agree to consistency tolerance. + + To make the warm-start path actually exercise the iteration-to-iteration + carry, the fake ``O`` matrix is varied per call (using a counter that is + reset between the two ``run_once`` invocations so both branches see the + same input sequence). + """ + from mpi4py import MPI as _MPI + + if _MPI.COMM_WORLD.Get_size() != 1: + pytest.skip("Numerical-agreement test runs single-process only.") + + ( + structure_data, + _, + _, + _, + geminal_mo_data, + coulomb_potential_data, + ) = read_trexio_file( + trexio_file=os.path.join(os.path.dirname(__file__), "trexio_example_files", trexio_file), store_tuple=True + ) + + jastrow_onebody_data = Jastrow_one_body_data.init_jastrow_one_body_data( + jastrow_1b_param=1.0, + structure_data=structure_data, + core_electrons=tuple([0] * len(structure_data.atomic_numbers)), + jastrow_1b_type="pade", + ) + jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="pade") + jastrow_data = Jastrow_data( + jastrow_one_body_data=jastrow_onebody_data, + jastrow_two_body_data=jastrow_twobody_data, + jastrow_three_body_data=None, + jastrow_nn_data=None, + ) + wavefunction_data = Wavefunction_data(jastrow_data=jastrow_data, geminal_data=geminal_mo_data) + hamiltonian_data = Hamiltonian_data( + structure_data=structure_data, + coulomb_potential_data=coulomb_potential_data, + wavefunction_data=wavefunction_data, + ) + + num_walkers = 2 + Dt = 2.0 + mcmc_seed = 12345 + epsilon_AS = 1.0e-6 + num_opt_steps = 3 + + base_params: dict[str, np.ndarray] = { + "j1_param": np.ones_like(np.array(jastrow_onebody_data.jastrow_1b_param)), + "j2_param": np.ones_like(np.array(jastrow_twobody_data.jastrow_2b_param)), + } + fixed_param_size = sum(v.size for v in base_params.values()) + + if regime == "tall": + num_mcmc = 1 + min_samples_total = num_mcmc * num_walkers + lambda_size_needed = max(1, min_samples_total - fixed_param_size + 1) + base_params["lambda_matrix"] = np.ones(lambda_size_needed, dtype=float) + else: + base_params["lambda_matrix"] = np.array([[2.0, -2.0], [3.0, -3.0]], dtype=float) + total_params_tmp = sum(v.size for v in base_params.values()) + num_mcmc = total_params_tmp // num_walkers + 2 + + fake_w_L_data = np.ones((num_mcmc, num_walkers)) + rng = np.random.default_rng(42) + fake_e_L_data = rng.standard_normal((num_mcmc, num_walkers)) * 0.1 + + # Single-slot holder for the live params dict. We can't key by ``id(wf)`` + # because ``MCMC.hamiltonian_data`` setter calls ``apply_diff_mask`` which + # rewraps the wavefunction with a fresh instance every time it's reassigned + # (i.e. at the end of every optimization iteration). The single-slot + # approach assumes one MCMC instance is alive at a time inside this test. + params_holder: dict[str, dict[str, np.ndarray] | None] = {"params": None} + + def register_params(_wf, params): + params_holder["params"] = params + + def lookup_params(_wf): + return params_holder["params"] + + # Counter that varies the fake O matrix per get_dln_WF call, so successive + # SR systems differ and CG warm-start has actual work to do. Reset between + # the two run_once invocations so both branches see identical input streams. + call_idx = {"count": 0} + + def fake_get_variational_blocks( + self, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=True, + opt_JNN_param=True, + opt_lambda_param=False, + opt_J3_basis_exp=False, + opt_J3_basis_coeff=False, + opt_lambda_basis_exp=False, + opt_lambda_basis_coeff=False, + ): + blocks = [] + pos = lookup_params(self) + if opt_J1_param and "j1_param" in pos: + arr = pos["j1_param"] + blocks.append(VariationalParameterBlock(name="j1_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_J2_param and "j2_param" in pos: + arr = pos["j2_param"] + blocks.append(VariationalParameterBlock(name="j2_param", values=arr, shape=arr.shape, size=int(arr.size))) + if opt_lambda_param and "lambda_matrix" in pos: + arr = pos["lambda_matrix"] + blocks.append(VariationalParameterBlock(name="lambda_matrix", values=arr, shape=arr.shape, size=int(arr.size))) + return blocks + + def fake_apply_block_updates(self, blocks, thetas, learning_rate): + params = lookup_params(self) + idx = 0 + for block in blocks: + blk_slice = thetas[idx : idx + block.size] + idx += block.size + if blk_slice.size == 0: + continue + delta = blk_slice.reshape(block.shape) + params[block.name] = params[block.name] + learning_rate * delta + return self + + def fake_run(self, num_mcmc_steps: int = 0, max_time=None): + return None + + def fake_get_dln_WF( + self, + blocks, + num_mcmc_warmup_steps=0, + chosen_param_index=None, + lambda_projectors=None, + num_orb_projection=None, + ): + call_idx["count"] += 1 + total = sum(block.size for block in blocks) + rng_local = np.random.default_rng(123 + call_idx["count"]) + return rng_local.standard_normal((num_mcmc, self.num_walkers, total)) * 0.01 + + def fake_get_E(self, num_mcmc_warmup_steps: int = 0, num_mcmc_bin_blocks: int = 1): + return (0.0, 0.0, 0.0, 0.0) + + def fake_get_gF( + self, + num_mcmc_warmup_steps, + num_mcmc_bin_blocks, + blocks, + lambda_projectors=None, + num_orb_projection=None, + chosen_param_index=None, + ): + total = sum(block.size for block in blocks) + return np.ones(total, dtype=float), np.ones(total, dtype=float) + + monkeypatch.setattr(Wavefunction_data, "get_variational_blocks", fake_get_variational_blocks, raising=False) + monkeypatch.setattr(Wavefunction_data, "apply_block_updates", fake_apply_block_updates, raising=False) + monkeypatch.setattr(MCMC, "run", fake_run, raising=False) + monkeypatch.setattr(MCMC, "get_E", fake_get_E, raising=False) + monkeypatch.setattr(MCMC, "get_gF", fake_get_gF, raising=False) + monkeypatch.setattr(MCMC, "get_dln_WF", fake_get_dln_WF, raising=False) + monkeypatch.setattr(MCMC, "w_L", property(lambda self: fake_w_L_data), raising=False) + monkeypatch.setattr(MCMC, "e_L", property(lambda self: fake_e_L_data), raising=False) + + def run_once(use_device_collectives: bool): + call_idx["count"] = 0 # reset so both branches see the same per-iter inputs + mcmc = MCMC( + hamiltonian_data=hamiltonian_data, + Dt=Dt, + mcmc_seed=mcmc_seed, + epsilon_AS=epsilon_AS, + num_walkers=num_walkers, + comput_position_deriv=False, + comput_log_WF_param_deriv=True, + comput_e_L_param_deriv=False, + random_discretized_mesh=True, + ) + params = {k: v.copy() for k, v in base_params.items()} + register_params(mcmc.hamiltonian_data.wavefunction_data, params) + mcmc.run_optimize( + num_mcmc_steps=num_mcmc, + num_opt_steps=num_opt_steps, + num_mcmc_warmup_steps=0, + num_mcmc_bin_blocks=1, + opt_J1_param=True, + opt_J2_param=True, + opt_J3_param=False, + opt_JNN_param=False, + opt_lambda_param=True, + optimizer_kwargs={ + "method": "sr", + "delta": 1.0e-3, + "epsilon": 1.0e-3, + "cg_flag": True, + "cg_max_iter": 200, + "cg_tol": 1.0e-12, + }, + use_device_collectives=use_device_collectives, + ) + return params + + cpu_params = run_once(use_device_collectives=False) + dev_params = run_once(use_device_collectives=True) + + for key in cpu_params: + cpu_v = cpu_params[key] + dev_v = dev_params[key] + # Sanity: 3 iters of warm-started CG produced a non-trivial param trail. + assert not np.array_equal(cpu_v, base_params[key]), f"baseline CPU update is trivial for {key}" + np.testing.assert_allclose( + dev_v, + cpu_v, + atol=atol_consistency, + rtol=rtol_consistency, + err_msg=f"device vs CPU CG warm-start mismatch for {key} (regime={regime})", + ) + + jax.clear_caches() + + @pytest.mark.parametrize("trexio_file", ["H2_ae_ccpvtz_cart.h5"]) def test_opt_with_projected_MOs(trexio_file, monkeypatch): """After run_optimize with opt_with_projected_MOs=True the final wavefunction From e22d256e79c12a40da3c30337c3eafdf6ffdcc42 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Mon, 11 May 2026 09:18:16 +0900 Subject: [PATCH 65/97] Shorten jqmc-run-full-pytest.yml --- .github/workflows/jqmc-run-full-pytest.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index 91d27792..2744aa70 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -65,13 +65,15 @@ jobs: pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # Skipped under full mode: + # pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP32+FP64 (Intra-software comparisons) run: | - pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # Skipped under mixed mode: precision-insensitive (coverage already obtained in FP64 block). + # pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed @@ -81,9 +83,10 @@ jobs: pytest -s -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # Skipped under mixed mode: HDF5 roundtrip tests, precision-insensitive (coverage already obtained in FP64 block). + # pytest -s -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed From f95aba1e9f407bdf43e70e0e810e40a616766663 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 14 May 2026 07:36:03 +0900 Subject: [PATCH 66/97] Optimize J2 ratio: O(N^2) baseline -> O(N * N_grid) per-grid sums (#72) * Update docs. * Optimize J2 ratio: O(N^2) baseline -> O(N * N_grid) per-grid sums --- CONTRIBUTING.md | 9 +- README.md | 3 +- __init__.py | 0 doc/conf.py | 6 + doc/examples.md | 51 +++- jqmc/jastrow_factor.py | 32 +-- jqmc_workflow/_error_estimator.py | 94 +++---- jqmc_workflow/_input_generator.py | 69 +++-- jqmc_workflow/_job.py | 80 +++--- jqmc_workflow/_lrdmc_calibration.py | 78 +++--- jqmc_workflow/_output_parser.py | 93 +++---- jqmc_workflow/_results.py | 290 ++++++++++---------- jqmc_workflow/_state.py | 79 +++--- jqmc_workflow/_transfer.py | 82 +++--- jqmc_workflow/launcher.py | 131 +++++---- jqmc_workflow/lrdmc_ext_workflow.py | 279 +++++++++---------- jqmc_workflow/lrdmc_workflow.py | 354 ++++++++++++------------ jqmc_workflow/mcmc_workflow.py | 259 +++++++++--------- jqmc_workflow/vmc_workflow.py | 377 +++++++++++++------------- jqmc_workflow/wf_workflow.py | 95 +++---- jqmc_workflow/workflow.py | 406 +++++++++++++--------------- tests/test_jastrow.py | 38 ++- 22 files changed, 1403 insertions(+), 1502 deletions(-) delete mode 100644 __init__.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6bc76bce..4c215da6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,12 +27,13 @@ We are willing to sacrifice some computational speed to achieve these goals. To ### Testing -* Robust testing is central to `jQMC`. For every functionality, `jQMC` provides two implementations: +* Robust testing is central to `jQMC`. For every functionality, `jQMC` provides multiple implementations: 1. A **`_debug`** version, written for human readability and easy tracing of the logic flow. - 2. A **`_jax`** version, optimized and decorated with `@jit` for high performance (though its control flow may be less obvious). -* In **`pytest`**, we verify that `_debug` and `_jax` produce numerically identical results. -* **When adding any new method**, you must implement both `_debug` and `_jax` variants. Pull requests lacking either will not be approved. + 2. A **production** version (no suffix), optimized and decorated with `@jit` for high performance -- using analytical derivatives where derivatives are involved (though its control flow may be less obvious). + 3. Where applicable, an **`_auto`** version, which is also `@jit`-optimized but relies on `JAX`'s automatic differentiation instead of analytical derivatives. This serves as an independent cross-check of the analytical implementation. +* In **`pytest`**, we verify that the `_debug`, production, and (when present) `_auto` variants produce numerically identical results. +* **When adding any new method**, you must implement both the `_debug` and production variants. If the method involves derivatives, an `_auto` variant should also be provided as a cross-check. Pull requests lacking the required variants will not be approved. --- diff --git a/README.md b/README.md index 548291a1..45549c8d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ This combination of features makes **jQMC** a versatile and powerful tool for bo ## Known issues -- On CPUs, **`jQMC` is slower than other QMC packages written in compiled languages (e.g., C++ or Fortran). On GPUs, however, `jQMC` achieves performance comparable to (or even faster than) compiled-language QMC codes**, thanks to `JAX`'s just-in-time compilation and hardware-level optimizations. Please use **GPUs** with a large number of walkers to fully exploit the performance. - Periodic boundary condition calculations are not supoorted yet. It will be implemented in the future as `JAX` supports `complex128`. Work in progress. @@ -182,7 +181,7 @@ If you used `jQMC` in your reseach project, please cite the following articles. %volume = {}, %number = {}, %pages = {}, - year = {2025}, + year = {2026}, %doi = {} } ``` diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/doc/conf.py b/doc/conf.py index 521566a5..cbbf5e18 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -114,6 +114,9 @@ def _dedup_footnote(m): "sphinx.ext.todo", ] myst_enable_extensions = ["linkify", "dollarmath", "amsmath"] +# Auto-generate anchors for Markdown headings up to depth 4 so that +# in-document links like ``[text](#phase-management)`` resolve. +myst_heading_anchors = 4 # Napoleon: parse Google/NumPy style docstrings (used in this project) and # render attribute/parameter tables in autodoc pages. @@ -123,6 +126,9 @@ def _dedup_footnote(m): napoleon_use_ivar = True napoleon_attr_annotations = True napoleon_preprocess_types = True +# Custom section "Output Values" used by Workflow docstrings -- render as +# a parameter-style list (same as "Returns"). +napoleon_custom_sections = [("Output Values", "params_style")] # Move type hints from signatures into the description so Attributes/Parameters # tables remain readable and avoid duplicated type-only attribute listings. diff --git a/doc/examples.md b/doc/examples.md index 57474d38..22191a5e 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -358,7 +358,7 @@ max_time = 86400 # Maximum time in sec. restart = false restart_chk = "restart.h5" # Restart checkpoint file. If restart is True, this file is used. hamiltonian_h5 = "hamiltonian_data.h5" # Hamiltonian checkpoint file. If restart is False, this file is used. -verbosity = "low" # Verbosity level. "low" or "high" +verbosity = "high" # Verbosity level. "low" or "high" [vmc] num_mcmc_steps = 500 # Number of observable measurement steps per MPI and Walker. Every local energy and other observeables are measured num_mcmc_steps times in total. The total number of measurements is num_mcmc_steps * mpi_size * number_of_walkers. @@ -376,6 +376,10 @@ opt_J3_param = true opt_JNN_param = false opt_lambda_param = false opt_with_projected_MOs = false +opt_J3_basis_exp = true +opt_J3_basis_coeff = true +opt_lambda_basis_exp = false +opt_lambda_basis_coeff = false ``` Please lunch the job. @@ -420,7 +424,7 @@ The important criteria are `Max f` and `Max of signal to noise of f`. `Max f` sh > [!TIP] > If the optimization does not converge well, try the following: > - Adjust the `delta` parameter in `optimizer_kwargs`. A smaller `delta` (e.g., `0.05`) makes the optimization more conservative but stable, while a larger one (e.g., `0.30`) is more aggressive but may cause instabilities. -> - Set `use_lm = false` in `optimizer_kwargs` to disable the linear method and use plain SR with a fixed step size instead. This can sometimes improve convergence for difficult cases. +> - Set `use_lm = false` in `optimizer_kwargs` to disable the Linear Method (LM) and use a fixed step size instead. This can sometimes improve convergence for difficult cases. You can also plot them and make a figure. @@ -697,6 +701,10 @@ opt_J3_param = false opt_JNN_param = true opt_lambda_param = false opt_with_projected_MOs = false +opt_J3_basis_exp = false +opt_J3_basis_coeff = false +opt_lambda_basis_exp = false +opt_lambda_basis_coeff = false optimizer_kwargs = { method = "adam" } ``` @@ -787,6 +795,7 @@ num_mcmc_bin_blocks = 5 # Number of blocks for binning per MPI and Walker. i.e., Dt = 2.0 # Step size for the MCMC update (bohr). epsilon_AS = 0.0 # the epsilon parameter used in the Attacalite-Sandro regulatization method. atomic_force = true +use_swct = true # Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. ``` The final step is to run the `jqmc` job w/ or w/o MPI on a CPU or GPU machine (via a job queueing system such as PBS). @@ -911,6 +920,10 @@ opt_J3_param = true opt_JNN_param = false opt_lambda_param = true opt_with_projected_MOs = true +opt_J3_basis_exp = false +opt_J3_basis_coeff = false +opt_lambda_basis_exp = false +opt_lambda_basis_coeff = false ``` The key differences from `example01` are: @@ -937,7 +950,7 @@ The important criteria are `Max f` and `Max of signal to noise of f`. `Max f` sh > [!TIP] > If the optimization does not converge well, try the following: > - Adjust the `delta` parameter in `optimizer_kwargs`. A smaller `delta` (e.g., `0.05`) makes the optimization more conservative but stable, while a larger one (e.g., `0.30`) is more aggressive but may cause instabilities. -> - Set `use_lm = false` in `optimizer_kwargs` to disable the linear method and use plain SR with a fixed step size instead. This can sometimes improve convergence for difficult cases. +> - Set `use_lm = false` in `optimizer_kwargs` to disable the Linear Method (LM) and use a fixed step size instead. This can sometimes improve convergence for difficult cases. You can also plot them and make a figure. @@ -1230,6 +1243,10 @@ opt_J3_param = true opt_JNN_param = false opt_lambda_param = false opt_with_projected_MOs = false +opt_J3_basis_exp = false +opt_J3_basis_coeff = false +opt_lambda_basis_exp = false +opt_lambda_basis_coeff = false ``` Please lunch the job. @@ -1277,7 +1294,7 @@ The important criteria are `Max f` and `Max of signal to noise of f`. `Max f` sh > [!TIP] > If the optimization does not converge well, try the following: > - Adjust the `delta` parameter in `optimizer_kwargs`. A smaller `delta` (e.g., `0.05`) makes the optimization more conservative but stable, while a larger one (e.g., `0.30`) is more aggressive but may cause instabilities. -> - Set `use_lm = false` in `optimizer_kwargs` to disable the linear method and use plain SR with a fixed step size instead. This can sometimes improve convergence for difficult cases. +> - Set `adaptive_learning_rate = false` in `optimizer_kwargs` to disable the adaptive learning rate and use a fixed step size instead. This can sometimes improve convergence for difficult cases. You can also plot them and make a figure. @@ -1450,6 +1467,10 @@ opt_J3_param = true opt_JNN_param = false opt_lambda_param = true opt_with_projected_MOs = false +opt_J3_basis_exp = false +opt_J3_basis_coeff = false +opt_lambda_basis_exp = false +opt_lambda_basis_coeff = false ``` > [!IMPORTANT] @@ -1751,6 +1772,10 @@ opt_J1_param = true opt_J2_param = true opt_J3_param = true opt_lambda_param = false +opt_J3_basis_exp = false +opt_J3_basis_coeff = false +opt_lambda_basis_exp = false +opt_lambda_basis_coeff = false ``` Please launch the job. @@ -1830,6 +1855,7 @@ num_mcmc_bin_blocks = 5 # Number of blocks for binning per MPI and Walker. i.e., Dt = 1.2 # Step size for the MCMC update (bohr). epsilon_AS = 0.0 # the epsilon parameter used in the Attacalite-Sandro regulatization method. atomic_force = true +use_swct = true # Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. ``` Run the `jqmc` job w/ or w/o MPI on a CPU or GPU machine (via a job queueing system such as PBS). @@ -1891,6 +1917,7 @@ num_gfmc_bin_blocks = 10 # Number of blocks for binning per MPI and Walker. i.e. num_gfmc_collect_steps = 5 # Number of measurement (before binning) for collecting the weights. E_scf = -1.0 # The initial guess of the total energy. This is used to compute the initial energy shift in the GFMC. atomic_force = true +use_swct = false # Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. Default is false for LRDMC. ``` Run the `jqmc` job w/ or w/o MPI on a CPU or GPU machine (via a job queueing system such as PBS). @@ -1964,6 +1991,10 @@ opt_J1_param = true opt_J2_param = true opt_J3_param = true opt_lambda_param = true +opt_J3_basis_exp = false +opt_J3_basis_coeff = false +opt_lambda_basis_exp = false +opt_lambda_basis_coeff = false ``` Please launch the job. @@ -2039,6 +2070,7 @@ num_mcmc_bin_blocks = 5 # Number of blocks for binning per MPI and Walker. i.e., Dt = 1.2 # Step size for the MCMC update (bohr). epsilon_AS = 0.0 # the epsilon parameter used in the Attacalite-Sandro regulatization method. atomic_force = true +use_swct = true # Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. ``` Run the `jqmc` job w/ or w/o MPI on a CPU or GPU machine (via a job queueing system such as PBS). @@ -2097,6 +2129,7 @@ num_gfmc_bin_blocks = 10 # Number of blocks for binning per MPI and Walker. i.e. num_gfmc_collect_steps = 5 # Number of measurement (before binning) for collecting the weights. E_scf = -1.0 # The initial guess of the total energy. This is used to compute the initial energy shift in the GFMC. atomic_force = true +use_swct = false # Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. Default is false for LRDMC. ``` Run the `jqmc` job w/ or w/o MPI on a CPU or GPU machine (via a job queueing system such as PBS). @@ -2538,7 +2571,7 @@ The workflow DAG is constructed programmatically in `run_pipelines.py` and execu #### Ansatz and optimization - **JSD** (Jastrow-Slater Determinant): J2 (two-body, exponential) + J3 (three-body, AO-small basis). No J1. -- **VMC optimization**: 50 steps with SR optimizer (`use_lm = True`, `delta = 0.35`) +- **VMC optimization**: 50 steps with SR optimizer (`adaptive_learning_rate = True`, `delta = 0.35`) - Determinant part is **not** optimized (`opt_with_projected_MOs = False`) #### Walker counts @@ -2631,7 +2664,7 @@ VMC_Workflow( "method": "sr", "delta": 0.350, "epsilon": 0.001, - "use_lm": True, + "adaptive_learning_rate": True, }, ) ``` @@ -2750,7 +2783,7 @@ The workflow DAG is constructed programmatically in `run_pipelines.py` and execu #### Ansatz and optimization - **JSD** (Jastrow-Slater Determinant): J2 (two-body, exponential) + J3 (three-body, AO-small basis). No J1. -- **VMC optimization**: 100 steps with SR optimizer (`use_lm = True`, `delta = 0.35`) +- **VMC optimization**: 100 steps with SR optimizer (`adaptive_learning_rate = True`, `delta = 0.35`) - Determinant part is **not** optimized (`opt_with_projected_MOs = False`) - 1024 walkers per MPI process @@ -2838,7 +2871,7 @@ VMC_Workflow( "method": "sr", "delta": 0.350, "epsilon": 0.001, - "use_lm": True, + "adaptive_learning_rate": True, }, max_time=76000, max_continuation=2, @@ -2859,7 +2892,6 @@ MCMC_Workflow( atomic_force=True, max_time=76000, max_continuation=2, - cleanup_patterns=["restart.h5"], # delete large checkpoint on success ) LRDMC_Workflow( @@ -2872,7 +2904,6 @@ LRDMC_Workflow( num_gfmc_collect_steps=20, max_time=76000, max_continuation=2, - cleanup_patterns=["restart.h5"], # delete large checkpoint on success ) ``` diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 013ae684..79b333e3 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -2449,10 +2449,6 @@ def dn_case(jastrow_2b_param, new_r_up_carts, new_r_dn_carts, old_r_up_carts, ol j2_param = jastrow_data.jastrow_two_body_data.jastrow_2b_param _j2_type = jastrow_data.jastrow_two_body_data.jastrow_2b_type - def _safe_norm(diff): - sq = jnp.sum(diff**2, axis=-1) - return jnp.where(sq > 0, jnp.sqrt(jnp.where(sq > 0, sq, jnp.ones_like(sq))), jnp.zeros_like(sq)) - if _j2_type == "pade": def _j2_from_dist(dist, param): @@ -2462,19 +2458,13 @@ def _j2_from_dist(dist, param): def _j2_from_dist(dist, param): return 1.0 / (2.0 * param) * (1.0 - jnp.exp(-param * dist)) - def compute_pairwise_sums(pos1, pos2): - if pos1.shape[0] == 0 or pos2.shape[0] == 0: - return jnp.zeros(pos1.shape[0], dtype=dtype_jnp) - # Reconstruct diff in caller-supplied precision then downcast (Principle 3b). - diff = (pos1[:, None, :] - pos2[None, :, :]).astype(dtype_jnp) - dists = _safe_norm(diff) - vals = _j2_from_dist(dists, j2_param) - return jnp.sum(vals, axis=1) - - J2_sum_up_up = compute_pairwise_sums(old_r_up_carts, old_r_up_carts) - J2_sum_up_dn = compute_pairwise_sums(old_r_up_carts, old_r_dn_carts) - J2_sum_dn_dn = compute_pairwise_sums(old_r_dn_carts, old_r_dn_carts) - J2_sum_dn_up = compute_pairwise_sums(old_r_dn_carts, old_r_up_carts) + # The OLD-side per-electron pair-sum baseline previously computed via + # ``compute_pairwise_sums(old, old)`` was O(N^2), but only the moved- + # electron row is consumed per grid. We now compute the OLD-side per- + # grid sum on demand via the same ``_batch_pairwise_sum`` used for the + # NEW side -- total work O(N * N_grid). The self-pair (j == k_g) on + # the OLD side contributes ``J2(0) = 0`` for both Pade and exp forms, + # so no explicit self-correction is needed for the OLD side. delta_up_all = new_r_up_carts_arr - old_r_up_carts delta_dn_all = new_r_dn_carts_arr - old_r_dn_carts @@ -2509,10 +2499,10 @@ def _batch_pairwise_sum(points_a, points_b, param): # Reconstruct diff in caller-supplied precision then downcast (Principle 3b). up_up_self = _j2_from_dist(jnp.linalg.norm((r_up_new - r_up_old).astype(dtype_jnp), axis=1), j2_param) up_up_new = up_up_new_raw - up_up_self - up_up_old = jnp.take(J2_sum_up_up, idx_up, axis=0) + up_up_old = _batch_pairwise_sum(r_up_old, old_r_up_carts, j2_param) up_dn_new = _batch_pairwise_sum(r_up_new, old_r_dn_carts, j2_param) - up_dn_old = jnp.take(J2_sum_up_dn, idx_up, axis=0) + up_dn_old = _batch_pairwise_sum(r_up_old, old_r_dn_carts, j2_param) J2_ratio_up = jnp.exp((up_up_new - up_up_old) + (up_dn_new - up_dn_old)) # Down-move branch contributions (all grids in batch) @@ -2520,10 +2510,10 @@ def _batch_pairwise_sum(points_a, points_b, param): # Reconstruct diff in caller-supplied precision then downcast (Principle 3b). dn_dn_self = _j2_from_dist(jnp.linalg.norm((r_dn_new - r_dn_old).astype(dtype_jnp), axis=1), j2_param) dn_dn_new = dn_dn_new_raw - dn_dn_self - dn_dn_old = jnp.take(J2_sum_dn_dn, idx_dn, axis=0) + dn_dn_old = _batch_pairwise_sum(r_dn_old, old_r_dn_carts, j2_param) dn_up_new = _batch_pairwise_sum(r_dn_new, old_r_up_carts, j2_param) - dn_up_old = jnp.take(J2_sum_dn_up, idx_dn, axis=0) + dn_up_old = _batch_pairwise_sum(r_dn_old, old_r_up_carts, j2_param) J2_ratio_dn = jnp.exp((dn_dn_new - dn_dn_old) + (dn_up_new - dn_up_old)) J2_ratio = jnp.where(moved_up_exists, J2_ratio_up, J2_ratio_dn) diff --git a/jqmc_workflow/_error_estimator.py b/jqmc_workflow/_error_estimator.py index b6d9b957..4b9cdbb9 100644 --- a/jqmc_workflow/_error_estimator.py +++ b/jqmc_workflow/_error_estimator.py @@ -70,27 +70,25 @@ def estimate_required_steps( \\times (\\sigma_{\\text{pilot}} / \\sigma_{\\text{target}})^2 \\times W_{\\text{pilot}} / W_{\\text{prod}} - Parameters - ---------- - pilot_steps : int - Number of measurement steps used in the pilot run. - pilot_error : float - Statistical error (standard error) obtained from the pilot run. - target_error : float - Desired statistical error for the production run. - walker_ratio : float - Ratio of effective walker counts: ``pilot_walkers / prod_walkers``. - When walkers-per-MPI is constant this equals - ``pilot_num_mpi / prod_num_mpi``. - Default 1.0 (same queue for pilot and production). - min_steps : int - Minimum number of steps to return (e.g. warmup + bin_blocks). - Default 0 (no minimum). + Args: + pilot_steps (int): + Number of measurement steps used in the pilot run. + pilot_error (float): + Statistical error (standard error) obtained from the pilot run. + target_error (float): + Desired statistical error for the production run. + walker_ratio (float): + Ratio of effective walker counts: ``pilot_walkers / prod_walkers``. + When walkers-per-MPI is constant this equals + ``pilot_num_mpi / prod_num_mpi``. + Default 1.0 (same queue for pilot and production). + min_steps (int): + Minimum number of steps to return (e.g. warmup + bin_blocks). + Default 0 (no minimum). Returns: - ------- - int - Estimated number of steps for the production run. + int: + Estimated number of steps for the production run. """ if target_error <= 0: raise ValueError(f"target_error must be positive, got {target_error}") @@ -132,21 +130,19 @@ def estimate_additional_steps( N_total = ceil( accumulated_steps x (current_error / target_error)^2 ) additional = N_total - accumulated_steps - Parameters - ---------- - accumulated_steps : int - Total measurement steps already in the checkpoint. - current_error : float - Statistical error after *accumulated_steps*. - target_error : float - Desired statistical error. - min_additional : int - Floor on additional steps to avoid trivially short runs. + Args: + accumulated_steps (int): + Total measurement steps already in the checkpoint. + current_error (float): + Statistical error after *accumulated_steps*. + target_error (float): + Desired statistical error. + min_additional (int): + Floor on additional steps to avoid trivially short runs. Returns: - ------- - int - Number of *additional* steps to run. + int: + Number of *additional* steps to run. """ if target_error <= 0: raise ValueError(f"target_error must be positive, got {target_error}") @@ -172,11 +168,10 @@ def suffixed_name(filename: str, index: int) -> str: """Insert an integer suffix before the file extension. Examples: - -------- - >>> suffixed_name("input.toml", 0) - 'input_0.toml' - >>> suffixed_name("out.o", 2) - 'out_2.o' + >>> suffixed_name("input.toml", 0) + 'input_0.toml' + >>> suffixed_name("out.o", 2) + 'out_2.o' """ base, ext = os.path.splitext(filename) return f"{base}_{index}{ext}" @@ -186,13 +181,12 @@ def _format_duration(seconds: float) -> str: """Format a duration in seconds as a human-readable string. Examples: - -------- - >>> _format_duration(90) - '1m 30s' - >>> _format_duration(3661) - '1h 1m 1s' - >>> _format_duration(86400) - '1d 0h 0m' + >>> _format_duration(90) + '1m 30s' + >>> _format_duration(3661) + '1h 1m 1s' + >>> _format_duration(86400) + '1d 0h 0m' """ seconds = max(0, seconds) if seconds < 60: @@ -236,15 +230,13 @@ def parse_net_time(output_file: str) -> float | None: Net GFMC time without pre-compilations = sec. (LRDMC) Net total time for MCMC = sec. (MCMC/VMC) - Parameters - ---------- - output_file : str - Path to the jQMC stdout/stderr output file. + Args: + output_file (str): + Path to the jQMC stdout/stderr output file. Returns: - ------- - float or None - Net time in seconds, or *None* if the pattern is not found. + float or None: + Net time in seconds, or *None* if the pattern is not found. """ if not os.path.isfile(output_file): logger.debug(f"parse_net_time: file not found: {output_file}") diff --git a/jqmc_workflow/_input_generator.py b/jqmc_workflow/_input_generator.py index e3522453..5b1ee3f5 100644 --- a/jqmc_workflow/_input_generator.py +++ b/jqmc_workflow/_input_generator.py @@ -49,19 +49,17 @@ def resolve_with_defaults(section_name: str, explicit_params: dict) -> dict: """Resolve *None* values using ``jqmc_miscs`` defaults and log them. - Parameters - ---------- - section_name : str - TOML section name (``"vmc"``, ``"mcmc"``, ``"lrdmc-bra"``, ``"control"``). - explicit_params : dict - ``{param_name: value_or_None}``. *None* entries are replaced - by the corresponding default from ``cli_parameters``. + Args: + section_name (str): + TOML section name (``"vmc"``, ``"mcmc"``, ``"lrdmc-bra"``, ``"control"``). + explicit_params (dict): + ``{param_name: value_or_None}``. *None* entries are replaced + by the corresponding default from ``cli_parameters``. Returns: - ------- - dict - Resolved parameters with no *None* values (unless the default - itself is *None*, which means the field is required). + dict: + Resolved parameters with no *None* values (unless the default + itself is *None*, which means the field is required). """ defaults = cli_parameters.get(section_name, {}) resolved = {} @@ -82,15 +80,13 @@ def get_default_parameters(job_type: str) -> dict: The returned dict has two sections: ``"control"`` and the job-type section (e.g. ``"mcmc"``, ``"vmc"``, ``"lrdmc-bra"``, ``"lrdmc-tau"``). - Parameters - ---------- - job_type : str - One of ``"mcmc"``, ``"vmc"``, ``"lrdmc-bra"``, ``"lrdmc-tau"``. + Args: + job_type (str): + One of ``"mcmc"``, ``"vmc"``, ``"lrdmc-bra"``, ``"lrdmc-tau"``. Returns: - ------- - dict - ``{"control": {...}, job_type: {...}}`` + dict: + ``{"control": {...}, job_type: {...}}`` """ if job_type not in cli_parameters: raise ValueError( @@ -112,30 +108,27 @@ def generate_input_toml( ) -> str: """Generate a jqmc TOML input file. - Parameters - ---------- - job_type : str - One of ``"mcmc"``, ``"vmc"``, ``"lrdmc-bra"``, ``"lrdmc-tau"``. - overrides : dict, optional - Nested dict of values to override, e.g. - ``{"control": {"number_of_walkers": 8}, "mcmc": {"num_mcmc_steps": 1000}}``. - filename : str - Output filename. - with_comments : bool - If True, insert inline comments from ``cli_parameters["*_comments"]``. + Args: + job_type (str): + One of ``"mcmc"``, ``"vmc"``, ``"lrdmc-bra"``, ``"lrdmc-tau"``. + overrides (dict, optional): + Nested dict of values to override, e.g. + ``{"control": {"number_of_walkers": 8}, "mcmc": {"num_mcmc_steps": 1000}}``. + filename (str): + Output filename. + with_comments (bool): + If True, insert inline comments from ``cli_parameters["*_comments"]``. Returns: - ------- - str - The absolute path of the written file. + str: + The absolute path of the written file. Examples: - -------- - >>> generate_input_toml( - ... "mcmc", - ... overrides={"mcmc": {"num_mcmc_steps": 500, "Dt": 1.5}}, - ... filename="mcmc.toml", - ... ) + >>> generate_input_toml( + ... "mcmc", + ... overrides={"mcmc": {"num_mcmc_steps": 500, "Dt": 1.5}}, + ... filename="mcmc.toml", + ... ) """ params = get_default_parameters(job_type) overrides = overrides or {} diff --git a/jqmc_workflow/_job.py b/jqmc_workflow/_job.py index ca1430f7..1acc1116 100644 --- a/jqmc_workflow/_job.py +++ b/jqmc_workflow/_job.py @@ -56,17 +56,15 @@ def load_queue_data(server_machine_name: str, queue_label: str) -> dict: """Load a single queue section from ``queue_data.toml``. - Parameters - ---------- - server_machine_name : str - Machine name (directory under ``~/.jqmc_setting/``). - queue_label : str - Section key in ``queue_data.toml``. + Args: + server_machine_name (str): + Machine name (directory under ``~/.jqmc_setting/``). + queue_label (str): + Section key in ``queue_data.toml``. Returns: - ------- - dict - The TOML table for *queue_label*. + dict: + The TOML table for *queue_label*. """ machine = Machine(server_machine_name) cfg = get_config_dir() @@ -164,13 +162,12 @@ def __init__( def generate_script(self, submission_script: str = "submit.sh", *, work_dir=None): """Generate job submission script from template + queue_data.toml vars. - Parameters - ---------- - submission_script : str - Filename of the generated script (basename). - work_dir : str, optional - Directory where the script is written. When *None*, - falls back to the current working directory. + Args: + submission_script (str): + Filename of the generated script (basename). + work_dir (str, optional): + Directory where the script is written. When *None*, + falls back to the current working directory. """ cfg = get_config_dir() template_path = os.path.join( @@ -211,15 +208,14 @@ def replace_kw(lines, keyword, value): def job_submit(self, submission_script: str = "submit.sh", from_objects=None, *, work_dir=None): """Submit the job. - Parameters - ---------- - submission_script : str - Basename of the submit script in *work_dir*. - from_objects : list[str], optional - Basenames of extra files to upload. - work_dir : str, optional - Absolute path to the local job directory. When *None*, - falls back to ``os.getcwd()`` for backward compatibility. + Args: + submission_script (str): + Basename of the submit script in *work_dir*. + from_objects (list[str], optional): + Basenames of extra files to upload. + work_dir (str, optional): + Absolute path to the local job directory. When *None*, + falls back to ``os.getcwd()`` for backward compatibility. """ from_objects = from_objects or [] @@ -347,19 +343,18 @@ def jobnum_check(self) -> bool: def fetch_job(self, from_objects=None, exclude_patterns=None, *, work_dir=None, optional_patterns=None): """Fetch job results from the remote machine. - Parameters - ---------- - from_objects : list[str], optional - Basenames or glob patterns of files to download. - exclude_patterns : list[str], optional - Glob patterns to exclude. - work_dir : str, optional - Absolute path to the local job directory. When *None*, - falls back to ``os.getcwd()`` for backward compatibility. - optional_patterns : list[str], optional - Basenames or glob patterns of non-essential files. - Missing files matching these patterns produce a warning - instead of an error. + Args: + from_objects (list[str], optional): + Basenames or glob patterns of files to download. + exclude_patterns (list[str], optional): + Glob patterns to exclude. + work_dir (str, optional): + Absolute path to the local job directory. When *None*, + falls back to ``os.getcwd()`` for backward compatibility. + optional_patterns (list[str], optional): + Basenames or glob patterns of non-essential files. + Missing files matching these patterns produce a warning + instead of an error. """ from_objects = from_objects or [] exclude_patterns = exclude_patterns or [] @@ -394,11 +389,10 @@ def job_acct(self) -> tuple[str, str, str] | None: flags in the config. Returns: - ------- - tuple[str, str, str] | None - ``(command, stdout, stderr)`` on success. - ``None`` if ``jobacct`` is not configured, the machine does - not use a queuing system, or the command fails. + tuple[str, str, str] | None: + ``(command, stdout, stderr)`` on success. + ``None`` if ``jobacct`` is not configured, the machine does + not use a queuing system, or the command fails. """ if not self.server_machine.queuing: return None diff --git a/jqmc_workflow/_lrdmc_calibration.py b/jqmc_workflow/_lrdmc_calibration.py index 0a9feba1..f1972a2c 100644 --- a/jqmc_workflow/_lrdmc_calibration.py +++ b/jqmc_workflow/_lrdmc_calibration.py @@ -61,20 +61,17 @@ def get_num_electrons(hamiltonian_file: str) -> int: """Read the total number of electrons from a hamiltonian HDF5 file. - Parameters - ---------- - hamiltonian_file : str - Path to ``hamiltonian_data.h5``. + Args: + hamiltonian_file (str): + Path to ``hamiltonian_data.h5``. Returns: - ------- - int - Total electron count ``num_electron_up + num_electron_dn``. + int: + Total electron count ``num_electron_up + num_electron_dn``. Raises: - ------ - RuntimeError - If the electron counts cannot be found in the file. + RuntimeError: + If the electron counts cannot be found in the file. """ try: with h5py.File(hamiltonian_file, "r") as f: @@ -98,15 +95,13 @@ def parse_survived_walkers_ratio(output_file: str) -> float | None: ``Survived walkers ratio = %`` and returns the **last** occurrence as a fraction (0.0-1.0). - Parameters - ---------- - output_file : str - Path to the jqmc stdout file. + Args: + output_file (str): + Path to the jqmc stdout file. Returns: - ------- - float or None - Survived walkers ratio as a fraction, or *None* if not found. + float or None: + Survived walkers ratio as a fraction, or *None* if not found. """ last_value = None try: @@ -135,25 +130,22 @@ def fit_num_projection_per_measurement( fits a linear model :math:`f(x) = a x + b` via least squares and solves for the *x* at which :math:`f(x) = \text{target\_ratio}`. - Parameters - ---------- - x_values : list[int] - ``num_projection_per_measurement`` values used in calibration runs. - y_values : list[float] - Corresponding survived-walkers ratios (fractions, 0.0-1.0). - target_ratio : float - Target survived-walkers ratio (e.g. 0.97). + Args: + x_values (list[int]): + ``num_projection_per_measurement`` values used in calibration runs. + y_values (list[float]): + Corresponding survived-walkers ratios (fractions, 0.0-1.0). + target_ratio (float): + Target survived-walkers ratio (e.g. 0.97). Returns: - ------- - int - Optimal ``num_projection_per_measurement`` (rounded up to the nearest - even integer, minimum 2). + int: + Optimal ``num_projection_per_measurement`` (rounded up to the nearest + even integer, minimum 2). Raises: - ------ - RuntimeError - If the linear fit cannot determine a positive root. + RuntimeError: + If the linear fit cannot determine a positive root. """ if len(x_values) < 2 or len(y_values) < 2: raise ValueError(f"Need at least 2 data points, got {len(x_values)}") @@ -214,20 +206,18 @@ def scale_num_projection_per_measurement( \text{nmpm}(\text{alat}) = \text{nmpm\_ref} \times \left(\frac{\text{alat\_ref}}{\text{alat}}\right)^{2} - Parameters - ---------- - nmpm_ref : int - Calibrated ``num_projection_per_measurement`` at ``alat_ref``. - alat_ref : float - Reference lattice spacing (bohr). - alat : float - Target lattice spacing (bohr). + Args: + nmpm_ref (int): + Calibrated ``num_projection_per_measurement`` at ``alat_ref``. + alat_ref (float): + Reference lattice spacing (bohr). + alat (float): + Target lattice spacing (bohr). Returns: - ------- - int - Scaled ``num_projection_per_measurement`` (rounded up to nearest even - integer, minimum 2). + int: + Scaled ``num_projection_per_measurement`` (rounded up to nearest even + integer, minimum 2). """ raw = nmpm_ref * (alat_ref / alat) ** 2 result = max(2, int(math.ceil(raw))) diff --git a/jqmc_workflow/_output_parser.py b/jqmc_workflow/_output_parser.py index 5ec95774..48394b32 100644 --- a/jqmc_workflow/_output_parser.py +++ b/jqmc_workflow/_output_parser.py @@ -88,16 +88,14 @@ def parse_ufloat_short(text: str): * ``+3(8)e-05`` -- scientific notation with integer uncertainty * ``+3.9(3.5)e-05`` -- scientific notation with decimal uncertainty - Parameters - ---------- - text : str - A single token like ``"+0.0114(14)"``, ``"-1.23(4)"``, - ``"+3(8)e-05"``, or ``"+3.9(3.5)e-05"``. + Args: + text (str): + A single token like ``"+0.0114(14)"``, ``"-1.23(4)"``, + ``"+3(8)e-05"``, or ``"+3.9(3.5)e-05"``. Returns: - ------- - tuple - ``(value, uncertainty)`` or ``(None, None)`` on failure. + tuple: + ``(value, uncertainty)`` or ``(None, None)`` on failure. """ m = re.match(r"([+-]?\d+\.?\d*)\((\d+\.?\d*)\)([eE][+-]?\d+)?", text.strip()) if not m: @@ -136,16 +134,14 @@ def parse_force_table(text: str): H +0.0112(11) +0.0050(7) +0.0054(9) ------------------------------------------------ - Parameters - ---------- - text : str - Full stdout from ``jqmc-tool {mcmc,lrdmc} compute-force``. + Args: + text (str): + Full stdout from ``jqmc-tool {mcmc,lrdmc} compute-force``. Returns: - ------- - list of dict or None - Each dict: ``{label, Fx, Fx_err, Fy, Fy_err, Fz, Fz_err}``. - Returns *None* when no force data is found. + list of dict or None: + Each dict: ``{label, Fx, Fx_err, Fy, Fy_err, Fz, Fz_err}``. + Returns *None* when no force data is found. """ lines = text.splitlines() forces = [] @@ -543,9 +539,8 @@ def _parse_vmc_log_text(text: str) -> list: the first ``Optimization step`` header are ignored. Returns: - ------- - list of VMC_Step_Data - One entry per optimization step found. + list of VMC_Step_Data: + One entry per optimization step found. """ steps: list[VMC_Step_Data] = [] current: VMC_Step_Data | None = None @@ -658,15 +653,13 @@ def parse_vmc_output(work_dir: str) -> VMC_Diagnostic_Data: records, parses per-step data, and looks for ``hamiltonian_data_opt_step_*.h5``. - Parameters - ---------- - work_dir : str - Path to the VMC working directory. + Args: + work_dir (str): + Path to the VMC working directory. Returns: - ------- - VMC_Diagnostic_Data - Structured parse result containing per-step data and metadata. + VMC_Diagnostic_Data: + Structured parse result containing per-step data and metadata. """ result = VMC_Diagnostic_Data() @@ -775,15 +768,13 @@ def parse_mcmc_output(work_dir: str) -> MCMC_Diagnostic_Data: section (populated by jqmc-tool post-processing) or from stdout if ``jqmc-tool mcmc compute-energy`` output is present. - Parameters - ---------- - work_dir : str - Path to the MCMC working directory. + Args: + work_dir (str): + Path to the MCMC working directory. Returns: - ------- - MCMC_Diagnostic_Data - Structured parse result. + MCMC_Diagnostic_Data: + Structured parse result. """ result = MCMC_Diagnostic_Data() @@ -885,15 +876,13 @@ def parse_lrdmc_output(work_dir: str) -> LRDMC_Diagnostic_Data: and net GFMC time from stdout. Energy/error come from ``workflow_state.toml`` result section. - Parameters - ---------- - work_dir : str - Path to the LRDMC working directory. + Args: + work_dir (str): + Path to the LRDMC working directory. Returns: - ------- - LRDMC_Diagnostic_Data - Structured parse result. + LRDMC_Diagnostic_Data: + Structured parse result. """ result = LRDMC_Diagnostic_Data() @@ -998,15 +987,13 @@ def parse_lrdmc_ext_output(work_dir: str) -> LRDMC_Ext_Diagnostic_Data: Looks for ``For a -> 0 bohr: E = ...`` in the stdout of the extrapolation step. - Parameters - ---------- - work_dir : str - Path to the LRDMC extrapolation working directory. + Args: + work_dir (str): + Path to the LRDMC extrapolation working directory. Returns: - ------- - LRDMC_Ext_Diagnostic_Data - Structured parse result. + LRDMC_Ext_Diagnostic_Data: + Structured parse result. """ result = LRDMC_Ext_Diagnostic_Data() @@ -1077,15 +1064,13 @@ def parse_input_params(work_dir: str) -> Input_Parameters: ``actual_opt_steps`` is read from ``restart.h5`` when available (VMC only). - Parameters - ---------- - work_dir : str - Path to the workflow working directory. + Args: + work_dir (str): + Path to the workflow working directory. Returns: - ------- - Input_Parameters - Structured parameter data with per-input detail. + Input_Parameters: + Structured parameter data with per-input detail. """ result = Input_Parameters() diff --git a/jqmc_workflow/_results.py b/jqmc_workflow/_results.py index 752e08b7..c79f9c1d 100644 --- a/jqmc_workflow/_results.py +++ b/jqmc_workflow/_results.py @@ -65,32 +65,31 @@ class VMC_Step_Data: """Data for one VMC optimization step. Attributes: - ---------- - step : int - Optimization step number (``Optimization step = N/M`` -> N). - energy : float or None - Total energy ``E = X +- Y`` -> X (Ha). - energy_error : float or None - Energy statistical error -> Y (Ha). - max_force : float or None - Maximum force ``Max f = X +- Y`` -> X (Ha/a.u.). - max_force_error : float or None - Force error -> Y (Ha/a.u.). - signal_to_noise_ratio : float or None - ``Max of signal-to-noise of f = max(|f|/|std f|) = X``. - avg_walker_weight : float or None - ``Average of walker weights is X``. - acceptance_ratio : float or None - ``Acceptance ratio is X %`` -> X / 100. - total_time_sec : float or None - ``Total elapsed time for MCMC N steps. = X sec.`` - precompilation_time_sec : float or None - ``Pre-compilation time for MCMC = X sec.`` - net_time_sec : float or None - ``Net total time for MCMC = X sec.`` - timing_breakdown : dict - Per-MCMC-step timing breakdown (msec). Keys match the - jQMC log lines, e.g. ``"mcmc_update"``, ``"e_L"``, etc. + step (int): + Optimization step number (``Optimization step = N/M`` -> N). + energy (float or None): + Total energy ``E = X +- Y`` -> X (Ha). + energy_error (float or None): + Energy statistical error -> Y (Ha). + max_force (float or None): + Maximum force ``Max f = X +- Y`` -> X (Ha/a.u.). + max_force_error (float or None): + Force error -> Y (Ha/a.u.). + signal_to_noise_ratio (float or None): + ``Max of signal-to-noise of f = max(|f|/|std f|) = X``. + avg_walker_weight (float or None): + ``Average of walker weights is X``. + acceptance_ratio (float or None): + ``Acceptance ratio is X %`` -> X / 100. + total_time_sec (float or None): + ``Total elapsed time for MCMC N steps. = X sec.`` + precompilation_time_sec (float or None): + ``Pre-compilation time for MCMC = X sec.`` + net_time_sec (float or None): + ``Net total time for MCMC = X sec.`` + timing_breakdown (dict): + Per-MCMC-step timing breakdown (msec). Keys match the + jQMC log lines, e.g. ``"mcmc_update"``, ``"e_L"``, etc. """ step: int @@ -112,36 +111,35 @@ class VMC_Diagnostic_Data: """Aggregated parse result for an entire VMC optimization. Attributes: - ---------- - steps : list of VMC_Step_Data - Per-step data in chronological order. - total_opt_steps : int or None - Total optimization steps (``Optimization step = N/M`` -> M). - total_opt_time_sec : float or None - ``Total elapsed time for optimization N steps. = X sec.`` - opt_timing_breakdown : dict - Per-optimization-step timing breakdown (sec). Keys: - ``"mcmc_run"``, ``"get_E"``, ``"get_gF"``, ``"optimizer"``, - ``"param_update"``, ``"mpi_barrier"``, ``"misc"``. - optimized_hamiltonian : str or None - Path to the last ``hamiltonian_data_opt_step_*.h5`` file found. - restart_checkpoint : str or None - Restart file name from ``Dump restart checkpoint file(s) to X.``. - ``None`` if the line was not found (indicates abnormal termination). - num_mpi_processes : int or None - ``The number of MPI process = N.`` -> N. - num_walkers_per_process : int or None - ``The number of walkers assigned for each MPI process = N.`` -> N. - jax_backend : str or None - ``JAX backend = X.`` -> X (e.g. ``"gpu"``, ``"cpu"``). - Set to ``"cpu"`` when the log says - ``Running on CPUs or single GPU``. - jax_devices : list or None - Parsed list of global XLA device strings from - ``*** XLA Global devices recognized by JAX***`` line. - e.g. ``["CudaDevice(id=0)", "CudaDevice(id=1)"]``. - stderr_tail : str - Last portion of stderr (up to 200 lines). + steps (list of VMC_Step_Data): + Per-step data in chronological order. + total_opt_steps (int or None): + Total optimization steps (``Optimization step = N/M`` -> M). + total_opt_time_sec (float or None): + ``Total elapsed time for optimization N steps. = X sec.`` + opt_timing_breakdown (dict): + Per-optimization-step timing breakdown (sec). Keys: + ``"mcmc_run"``, ``"get_E"``, ``"get_gF"``, ``"optimizer"``, + ``"param_update"``, ``"mpi_barrier"``, ``"misc"``. + optimized_hamiltonian (str or None): + Path to the last ``hamiltonian_data_opt_step_*.h5`` file found. + restart_checkpoint (str or None): + Restart file name from ``Dump restart checkpoint file(s) to X.``. + ``None`` if the line was not found (indicates abnormal termination). + num_mpi_processes (int or None): + ``The number of MPI process = N.`` -> N. + num_walkers_per_process (int or None): + ``The number of walkers assigned for each MPI process = N.`` -> N. + jax_backend (str or None): + ``JAX backend = X.`` -> X (e.g. ``"gpu"``, ``"cpu"``). + Set to ``"cpu"`` when the log says + ``Running on CPUs or single GPU``. + jax_devices (list or None): + Parsed list of global XLA device strings from + ``*** XLA Global devices recognized by JAX***`` line. + e.g. ``["CudaDevice(id=0)", "CudaDevice(id=1)"]``. + stderr_tail (str): + Last portion of stderr (up to 200 lines). """ steps: list = field(default_factory=list) # list[VMC_Step_Data] @@ -165,43 +163,42 @@ class MCMC_Diagnostic_Data: """Parse result for an MCMC sampling run. Attributes: - ---------- - acceptance_ratio : float or None - ``Acceptance ratio is X %`` -> X / 100. - avg_walker_weight : float or None - ``Average of walker weights is X``. - total_time_sec : float or None - ``Total elapsed time for MCMC N steps. = X sec.`` - precompilation_time_sec : float or None - ``Pre-compilation time for MCMC = X sec.`` - net_time_sec : float or None - ``Net total time for MCMC = X sec.`` - timing_breakdown : dict - Per-MCMC-step timing breakdown (msec). Keys: ``"mcmc_update"``, - ``"e_L"``, ``"de_L_dR_dr"``, ``"dln_Psi_dR_dr"``, - ``"dln_Psi_dc"``, ``"de_L_dc"``, ``"mpi_barrier"``, ``"misc"``. - energy : float or None - Energy from jqmc-tool post-processing. - energy_error : float or None - Energy error from jqmc-tool post-processing. - atomic_forces : list of dict or None - Per-atom forces from ``jqmc-tool mcmc compute-force``. - Each dict: ``{label, Fx, Fx_err, Fy, Fy_err, Fz, Fz_err}``. - hamiltonian_data_file : str or None - ``[control] hamiltonian_h5`` value from the input TOML. - restart_checkpoint : str or None - Restart file name from ``Dump restart checkpoint file(s) to X.``. - ``None`` if the line was not found. - num_mpi_processes : int or None - ``The number of MPI process = N.`` -> N. - num_walkers_per_process : int or None - ``The number of walkers assigned for each MPI process = N.`` -> N. - jax_backend : str or None - ``JAX backend = X.`` -> X (e.g. ``"gpu"``, ``"cpu"``). - jax_devices : list or None - Parsed list of global XLA device strings. - stderr_tail : str - Last portion of stderr (up to 200 lines). + acceptance_ratio (float or None): + ``Acceptance ratio is X %`` -> X / 100. + avg_walker_weight (float or None): + ``Average of walker weights is X``. + total_time_sec (float or None): + ``Total elapsed time for MCMC N steps. = X sec.`` + precompilation_time_sec (float or None): + ``Pre-compilation time for MCMC = X sec.`` + net_time_sec (float or None): + ``Net total time for MCMC = X sec.`` + timing_breakdown (dict): + Per-MCMC-step timing breakdown (msec). Keys: ``"mcmc_update"``, + ``"e_L"``, ``"de_L_dR_dr"``, ``"dln_Psi_dR_dr"``, + ``"dln_Psi_dc"``, ``"de_L_dc"``, ``"mpi_barrier"``, ``"misc"``. + energy (float or None): + Energy from jqmc-tool post-processing. + energy_error (float or None): + Energy error from jqmc-tool post-processing. + atomic_forces (list of dict or None): + Per-atom forces from ``jqmc-tool mcmc compute-force``. + Each dict: ``{label, Fx, Fx_err, Fy, Fy_err, Fz, Fz_err}``. + hamiltonian_data_file (str or None): + ``[control] hamiltonian_h5`` value from the input TOML. + restart_checkpoint (str or None): + Restart file name from ``Dump restart checkpoint file(s) to X.``. + ``None`` if the line was not found. + num_mpi_processes (int or None): + ``The number of MPI process = N.`` -> N. + num_walkers_per_process (int or None): + ``The number of walkers assigned for each MPI process = N.`` -> N. + jax_backend (str or None): + ``JAX backend = X.`` -> X (e.g. ``"gpu"``, ``"cpu"``). + jax_devices (list or None): + Parsed list of global XLA device strings. + stderr_tail (str): + Last portion of stderr (up to 200 lines). """ acceptance_ratio: float | None = None @@ -230,44 +227,43 @@ class LRDMC_Diagnostic_Data: """Parse result for an LRDMC calculation. Attributes: - ---------- - survived_walkers_ratio : float or None - ``Survived walkers ratio = X %`` -> X / 100. - avg_num_projections : float or None - ``Average of the number of projections = X``. - total_time_sec : float or None - ``Total GFMC time for N branching steps = X sec.`` - precompilation_time_sec : float or None - ``Pre-compilation time for GFMC = X sec.`` - net_time_sec : float or None - ``Net GFMC time without pre-compilations = X sec.`` - timing_breakdown : dict - Per-branching timing breakdown (msec). Keys vary by LRDMC - variant, e.g. ``"projection"``, ``"observable"``, - ``"mpi_barrier"``, ``"collection"``, ``"reconfiguration"``, - ``"e_L"``, ``"de_L_dR_dr"``, ``"update_E_scf"``, ``"misc"``. - energy : float or None - Energy from jqmc-tool post-processing. - energy_error : float or None - Energy error from jqmc-tool post-processing. - atomic_forces : list of dict or None - Per-atom forces from ``jqmc-tool lrdmc compute-force``. - Each dict: ``{label, Fx, Fx_err, Fy, Fy_err, Fz, Fz_err}``. - hamiltonian_data_file : str or None - ``[control] hamiltonian_h5`` value from the input TOML. - restart_checkpoint : str or None - Restart file name from ``Dump restart checkpoint file(s) to X.``. - ``None`` if the line was not found. - num_mpi_processes : int or None - ``The number of MPI process = N.`` -> N. - num_walkers_per_process : int or None - ``The number of walkers assigned for each MPI process = N.`` -> N. - jax_backend : str or None - ``JAX backend = X.`` -> X (e.g. ``"gpu"``, ``"cpu"``). - jax_devices : list or None - Parsed list of global XLA device strings. - stderr_tail : str - Last portion of stderr (up to 200 lines). + survived_walkers_ratio (float or None): + ``Survived walkers ratio = X %`` -> X / 100. + avg_num_projections (float or None): + ``Average of the number of projections = X``. + total_time_sec (float or None): + ``Total GFMC time for N branching steps = X sec.`` + precompilation_time_sec (float or None): + ``Pre-compilation time for GFMC = X sec.`` + net_time_sec (float or None): + ``Net GFMC time without pre-compilations = X sec.`` + timing_breakdown (dict): + Per-branching timing breakdown (msec). Keys vary by LRDMC + variant, e.g. ``"projection"``, ``"observable"``, + ``"mpi_barrier"``, ``"collection"``, ``"reconfiguration"``, + ``"e_L"``, ``"de_L_dR_dr"``, ``"update_E_scf"``, ``"misc"``. + energy (float or None): + Energy from jqmc-tool post-processing. + energy_error (float or None): + Energy error from jqmc-tool post-processing. + atomic_forces (list of dict or None): + Per-atom forces from ``jqmc-tool lrdmc compute-force``. + Each dict: ``{label, Fx, Fx_err, Fy, Fy_err, Fz, Fz_err}``. + hamiltonian_data_file (str or None): + ``[control] hamiltonian_h5`` value from the input TOML. + restart_checkpoint (str or None): + Restart file name from ``Dump restart checkpoint file(s) to X.``. + ``None`` if the line was not found. + num_mpi_processes (int or None): + ``The number of MPI process = N.`` -> N. + num_walkers_per_process (int or None): + ``The number of walkers assigned for each MPI process = N.`` -> N. + jax_backend (str or None): + ``JAX backend = X.`` -> X (e.g. ``"gpu"``, ``"cpu"``). + jax_devices (list or None): + Parsed list of global XLA device strings. + stderr_tail (str): + Last portion of stderr (up to 200 lines). """ survived_walkers_ratio: float | None = None @@ -296,15 +292,14 @@ class LRDMC_Ext_Diagnostic_Data: """Parse result for an LRDMC a^2->0 extrapolation. Attributes: - ---------- - extrapolated_energy : float or None - ``For a -> 0 bohr: E = X +- Y Ha.`` -> X. - extrapolated_energy_error : float or None - Y from the above. - per_alat_results : list of dict - Each dict has ``{"alat": float, "energy": float, "energy_error": float}``. - stderr_tail : str - Last portion of stderr (up to 200 lines). + extrapolated_energy (float or None): + ``For a -> 0 bohr: E = X +- Y Ha.`` -> X. + extrapolated_energy_error (float or None): + Y from the above. + per_alat_results (list of dict): + Each dict has ``{"alat": float, "energy": float, "energy_error": float}``. + stderr_tail (str): + Last portion of stderr (up to 200 lines). """ extrapolated_energy: float | None = None @@ -335,14 +330,13 @@ class Input_Parameters: } Attributes: - ---------- - actual_opt_steps : int or None - For VMC: last completed optimization step stored in - ``restart.h5`` (``rank_0/driver_config`` attrs ``i_opt``). - ``None`` for non-VMC workflows. - per_input : list of dict - Per-input-file parameters. One dict per ``[[jobs]]`` entry - in ``workflow_state.toml``. + actual_opt_steps (int or None): + For VMC: last completed optimization step stored in + ``restart.h5`` (``rank_0/driver_config`` attrs ``i_opt``). + ``None`` for non-VMC workflows. + per_input (list of dict): + Per-input-file parameters. One dict per ``[[jobs]]`` entry + in ``workflow_state.toml``. """ actual_opt_steps: int | None = None diff --git a/jqmc_workflow/_state.py b/jqmc_workflow/_state.py index bfe7336a..aa4442a5 100644 --- a/jqmc_workflow/_state.py +++ b/jqmc_workflow/_state.py @@ -227,28 +227,26 @@ def validate_completion( 4. (target_error mode) ``energy_error > target_error * target_tol`` -> ``INCOMPLETE``; otherwise -> ``OK``. - Parameters - ---------- - directory : str - Working directory containing ``workflow_state.toml`` and fetched - output files. - output_values : dict, optional - Scalar results from the workflow (``energy``, ``energy_error``, - ...). - target_error : float, optional - Target statistical error in Ha. ``None`` disables the - convergence check (post-hoc mode). - target_tol : float, default 1.0 - Tolerance factor applied to ``target_error``. Callers that - previously accepted ``error <= target * 1.05`` (MCMC) or - ``error <= target * 1.20`` (LRDMC) pass the matching factor. + Args: + directory (str): + Working directory containing ``workflow_state.toml`` and fetched + output files. + output_values (dict, optional): + Scalar results from the workflow (``energy``, ``energy_error``, + ...). + target_error (float, optional): + Target statistical error in Ha. ``None`` disables the + convergence check (post-hoc mode). + target_tol (float, default 1.0): + Tolerance factor applied to ``target_error``. Callers that + previously accepted ``error <= target * 1.05`` (MCMC) or + ``error <= target * 1.20`` (LRDMC) pass the matching factor. Returns: - ------- - status : CompletionStatus - ``OK`` / ``FAILED`` / ``INCOMPLETE``. - message : str - Human-readable description; empty string when ``status == OK``. + status (CompletionStatus): + ``OK`` / ``FAILED`` / ``INCOMPLETE``. + message (str): + Human-readable description; empty string when ``status == OK``. """ output_values = output_values or {} state = read_state(directory) @@ -301,18 +299,17 @@ def update_status( ): """Update the status field (and optional extra fields) in workflow_state.toml. - Parameters - ---------- - directory : str - Working directory containing workflow_state.toml. - status : str or WorkflowStatus - New workflow status. - phase : str or None - Scientific phase to record. If given, written to - ``[workflow] phase``. - **extra_fields - Additional fields. Keys starting with ``result_`` go into - ``[result]``; everything else goes into ``[workflow]``. + Args: + directory (str): + Working directory containing workflow_state.toml. + status (str or WorkflowStatus): + New workflow status. + phase (str or None): + Scientific phase to record. If given, written to + ``[workflow] phase``. + **extra_fields: + Additional fields. Keys starting with ``result_`` go into + ``[result]``; everything else goes into ``[workflow]``. """ # Ensure we work with raw string for VALID_STATUSES check status_str = status.value if isinstance(status, WorkflowStatus) else status @@ -568,12 +565,11 @@ def get_workflow_summary(directory: str) -> dict: def set_error(directory: str, message: str, **context) -> None: """Write error information to the ``[error]`` section. - Parameters - ---------- - message : str - Human-readable error description (exception message, etc.). - **context - Arbitrary extra fields (``traceback``, ``exception_type``, ...). + Args: + message (str): + Human-readable error description (exception message, etc.). + **context: + Arbitrary extra fields (``traceback``, ``exception_type``, ...). """ state = read_state(directory) state["error"] = {"message": message, **context} @@ -652,10 +648,9 @@ def get_artifact_registry(directory: str) -> list[dict]: def set_input_fingerprints(directory: str, fingerprints: dict[str, dict]) -> None: """Record input-file fingerprints in ``[input_fingerprints]``. - Parameters - ---------- - fingerprints : dict[str, dict] - Mapping ``{basename: {"size": int, "mtime": float}}``. + Args: + fingerprints (dict[str, dict]): + Mapping ``{basename: {"size": int, "mtime": float}}``. """ state = read_state(directory) state["input_fingerprints"] = fingerprints diff --git a/jqmc_workflow/_transfer.py b/jqmc_workflow/_transfer.py index c7a90ea9..5f718d27 100644 --- a/jqmc_workflow/_transfer.py +++ b/jqmc_workflow/_transfer.py @@ -48,12 +48,11 @@ class Data_transfer: """Convenience layer over Machines_handler for local <-> remote transfers. - Parameters - ---------- - server_machine_name : str - Name of the server machine as defined in ~/.jqmc_setting/machine_data.yaml. - safe_mode : bool - If True, verify root directories exist before transfer. + Args: + server_machine_name (str): + Name of the server machine as defined in ~/.jqmc_setting/machine_data.yaml. + safe_mode (bool): + If True, verify root directories exist before transfer. """ def __init__(self, server_machine_name: str, safe_mode: bool = False): @@ -84,18 +83,17 @@ def ssh_close(self): def put_objects(self, from_objects=None, exclude_patterns=None, *, work_dir=None): """Upload files from *work_dir* to the corresponding remote directory. - Parameters - ---------- - from_objects : list[str], optional - Basenames or glob patterns of files to upload. When empty, - the entire *work_dir* is synced. - exclude_patterns : list[str], optional - Glob patterns to exclude from the transfer. - work_dir : str, optional - Local directory that maps to the remote workspace. When - *None*, falls back to ``os.getcwd()`` for backward - compatibility, but callers should always pass this - explicitly. + Args: + from_objects (list[str], optional): + Basenames or glob patterns of files to upload. When empty, + the entire *work_dir* is synced. + exclude_patterns (list[str], optional): + Glob patterns to exclude from the transfer. + work_dir (str, optional): + Local directory that maps to the remote workspace. When + *None*, falls back to ``os.getcwd()`` for backward + compatibility, but callers should always pass this + explicitly. """ from_objects = from_objects or [] exclude_patterns = exclude_patterns or [] @@ -158,23 +156,22 @@ def put_objects(self, from_objects=None, exclude_patterns=None, *, work_dir=None def get_objects(self, from_objects=None, exclude_patterns=None, *, work_dir=None, optional_patterns=None): """Download files from the remote directory to *work_dir*. - Parameters - ---------- - from_objects : list[str], optional - Basenames or glob patterns of files to download. When - empty, the entire remote directory is synced. - exclude_patterns : list[str], optional - Glob patterns to exclude from the transfer. - work_dir : str, optional - Local directory that maps to the remote workspace. When - *None*, falls back to ``os.getcwd()`` for backward - compatibility, but callers should always pass this - explicitly. - optional_patterns : list[str], optional - Basenames or glob patterns of files that are non-essential. - When a file matching one of these patterns is missing on - the server, a warning is logged instead of raising - ``FileNotFoundError``. + Args: + from_objects (list[str], optional): + Basenames or glob patterns of files to download. When + empty, the entire remote directory is synced. + exclude_patterns (list[str], optional): + Glob patterns to exclude from the transfer. + work_dir (str, optional): + Local directory that maps to the remote workspace. When + *None*, falls back to ``os.getcwd()`` for backward + compatibility, but callers should always pass this + explicitly. + optional_patterns (list[str], optional): + Basenames or glob patterns of files that are non-essential. + When a file matching one of these patterns is missing on + the server, a warning is logged instead of raising + ``FileNotFoundError``. """ from_objects = from_objects or [] exclude_patterns = exclude_patterns or [] @@ -248,14 +245,13 @@ def remove_objects(self, patterns: list[str], *, work_dir: str | None = None) -> Matching is **recursive** -- each pattern is applied to *work_dir* and all of its subdirectories (e.g. ``_pilot/``, ``_pilot_a/``). - Parameters - ---------- - patterns : list[str] - Glob patterns relative to *work_dir* (e.g. ``["restart.h5", - "hamiltonian_opt*.h5"]``). Each pattern is searched in - the top-level directory **and** all subdirectories. - work_dir : str, optional - Local directory. When *None*, falls back to ``os.getcwd()``. + Args: + patterns (list[str]): + Glob patterns relative to *work_dir* (e.g. ``["restart.h5", + "hamiltonian_opt*.h5"]``). Each pattern is searched in + the top-level directory **and** all subdirectories. + work_dir (str, optional): + Local directory. When *None*, falls back to ``os.getcwd()``. """ local_cwd = os.path.abspath(work_dir) if work_dir else os.path.abspath(os.getcwd()) diff --git a/jqmc_workflow/launcher.py b/jqmc_workflow/launcher.py index cb12757f..7fd94b5c 100644 --- a/jqmc_workflow/launcher.py +++ b/jqmc_workflow/launcher.py @@ -68,82 +68,77 @@ class Launcher: as **all** predecessors of a node complete, that node starts immediately -- there is no layer-based grouping. - Parameters - ---------- - workflows : list[Container] - Workflows to execute. Labels must be unique. - log_level : str - Logging level (``"DEBUG"`` or ``"INFO"``). - log_name : str - Log file name (appended, not overwritten). - draw_graph : bool - If ``True``, render the dependency graph to ``dependency_graph.png`` - (requires the ``graphviz`` Python package). + Args: + workflows (list[Container]): + Workflows to execute. Labels must be unique. + log_level (str): + Logging level (``"DEBUG"`` or ``"INFO"``). + log_name (str): + Log file name (appended, not overwritten). + draw_graph (bool): + If ``True``, render the dependency graph to ``dependency_graph.png`` + (requires the ``graphviz`` Python package). Raises: - ------ - ValueError - If workflow labels are duplicated or a dependency references an - undefined workflow label. + ValueError: + If workflow labels are duplicated or a dependency references an + undefined workflow label. Examples: - -------- - Typical three-stage QMC pipeline:: - - from jqmc_workflow import ( - Launcher, Container, FileFrom, - WF_Workflow, VMC_Workflow, MCMC_Workflow, - ) - - wf = Container( - label="wf", - dirname="00_wf", - input_files=["trexio.h5"], - workflow=WF_Workflow(trexio_file="trexio.h5"), - ) - - vmc = Container( - label="vmc-opt", - dirname="01_vmc", - input_files=[FileFrom("wf", "hamiltonian_data.h5")], - workflow=VMC_Workflow( - server_machine_name="cluster", - num_opt_steps=10, - target_error=0.001, - ), - ) - - mcmc = Container( - label="mcmc-run", - dirname="02_mcmc", - input_files=[ - FileFrom("vmc-opt", "hamiltonian_data_opt_step_9.h5") - ], - rename_input_files=["hamiltonian_data.h5"], - workflow=MCMC_Workflow( - server_machine_name="cluster", - target_error=0.001, - ), - ) - - launcher = Launcher( - workflows=[wf, vmc, mcmc], - draw_graph=True, - ) - launcher.launch() + Typical three-stage QMC pipeline:: + + from jqmc_workflow import ( + Launcher, Container, FileFrom, + WF_Workflow, VMC_Workflow, MCMC_Workflow, + ) + + wf = Container( + label="wf", + dirname="00_wf", + input_files=["trexio.h5"], + workflow=WF_Workflow(trexio_file="trexio.h5"), + ) + + vmc = Container( + label="vmc-opt", + dirname="01_vmc", + input_files=[FileFrom("wf", "hamiltonian_data.h5")], + workflow=VMC_Workflow( + server_machine_name="cluster", + num_opt_steps=10, + target_error=0.001, + ), + ) + + mcmc = Container( + label="mcmc-run", + dirname="02_mcmc", + input_files=[ + FileFrom("vmc-opt", "hamiltonian_data_opt_step_9.h5") + ], + rename_input_files=["hamiltonian_data.h5"], + workflow=MCMC_Workflow( + server_machine_name="cluster", + target_error=0.001, + ), + ) + + launcher = Launcher( + workflows=[wf, vmc, mcmc], + draw_graph=True, + ) + launcher.launch() Notes: - ----- - * The launcher changes the working directory during execution and - restores it afterwards. - * If a workflow fails, all downstream dependents are automatically - skipped. + * The launcher changes the working directory during execution and + restores it afterwards. + * If a workflow fails, all downstream dependents are automatically + skipped. See Also: - -------- - Container : Wraps a workflow in a project directory. - FileFrom : File dependency placeholder. - ValueFrom : Value dependency placeholder. + Container : Wraps a workflow in a project directory. + FileFrom : File dependency placeholder. + ValueFrom : Value dependency placeholder. """ def __init__( diff --git a/jqmc_workflow/lrdmc_ext_workflow.py b/jqmc_workflow/lrdmc_ext_workflow.py index faed141b..e1444646 100644 --- a/jqmc_workflow/lrdmc_ext_workflow.py +++ b/jqmc_workflow/lrdmc_ext_workflow.py @@ -80,151 +80,144 @@ class LRDMC_Ext_Workflow(Workflow): * **GFMC_n** -- set *target_survived_walkers_ratio* or *num_projection_per_measurement*. - Parameters - ---------- - server_machine_name : str - Target machine name (shared by all sub-runs). - alat_list : list[float] - List of lattice discretization values, e.g. ``[0.5, 0.4, 0.3]``. - hamiltonian_file : str - Input ``hamiltonian_data.h5`` (must exist in the parent directory - or be resolved by ``FileFrom``). - queue_label : str - Queue/partition label for production runs. - pilot_queue_label : str, optional - Queue/partition label for pilot runs. Defaults to - ``queue_label`` when *None*. A shorter queue is often - sufficient for the pilot. - jobname_prefix : str - Prefix for each sub-run job name. - number_of_walkers : int - Walkers per MPI process. - max_time : int - Wall-time limit per sub-run (seconds). - polynomial_order : int - Polynomial order for the a^2->0 extrapolation (default: 2). - num_gfmc_bin_blocks : int - Binning blocks for post-processing. - num_gfmc_warmup_steps : int - Warmup steps to discard. - num_gfmc_collect_steps : int - Weight-collection steps. - time_projection_tau : float, optional - Imaginary time step for GFMC_t mode (default 0.10). Ignored - when *target_survived_walkers_ratio* or - *num_projection_per_measurement* is set. - target_survived_walkers_ratio : float, optional - Target survived-walkers ratio (default *None*). Each ``alat`` - independently runs a calibration pilot (``_pilot_a``) to - find its own optimal ``num_projection_per_measurement``. - Set to *None* to disable auto-calibration (requires explicit - *num_projection_per_measurement*). Activates GFMC_n mode. - num_projection_per_measurement : int, optional - GFMC projections per measurement. When given explicitly, - automatic calibration is disabled and this value is used - for every ``alat``. Activates GFMC_n mode. - non_local_move : str, optional - Non-local move treatment. Default from ``jqmc_miscs``. - E_scf : float, optional - Initial energy guess for the GFMC shift (GFMC_n only). - Default from ``jqmc_miscs``. - atomic_force : bool, optional - Compute atomic forces. Default from ``jqmc_miscs``. - use_swct : bool, optional - Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. - Default is False for LRDMC. - epsilon_PW : float, optional - Pathak-Wagner regularization parameter (Bohr). When > 0, - the force estimator is regularized near the nodal surface. - Default from ``jqmc_miscs``. - mcmc_seed : int, optional - Random seed for MCMC. Default from ``jqmc_miscs``. - verbosity : str, optional - Verbosity level. Default from ``jqmc_miscs``. - poll_interval : int - Seconds between job-status polls. - target_error : float - Target statistical error (Ha) for each sub-LRDMC run. - Passed through to each :class:`LRDMC_Workflow`. - pilot_steps : int - Pilot measurement steps for target-error estimation. - num_gfmc_projections : int, optional - Fixed number of measurement steps per production run. - When set, the error-bar pilot is skipped for each sub-LRDMC - and all ``max_continuation`` runs are executed unconditionally. - Passed through to each :class:`LRDMC_Workflow`. - Default *None* (automatic mode). - max_continuation : int - Maximum number of production runs per sub-LRDMC. - cleanup_patterns : list[str], optional - Glob patterns for files to delete after successful completion - (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). Local files - are always removed; remote files are removed only when the - workflow targets a remote machine. Passed through to each - child :class:`LRDMC_Workflow`. Default *None* (no cleanup). + Args: + server_machine_name (str): + Target machine name (shared by all sub-runs). + alat_list (list[float]): + List of lattice discretization values, e.g. ``[0.5, 0.4, 0.3]``. + hamiltonian_file (str): + Input ``hamiltonian_data.h5`` (must exist in the parent directory + or be resolved by ``FileFrom``). + queue_label (str): + Queue/partition label for production runs. + pilot_queue_label (str, optional): + Queue/partition label for pilot runs. Defaults to + ``queue_label`` when *None*. A shorter queue is often + sufficient for the pilot. + jobname_prefix (str): + Prefix for each sub-run job name. + number_of_walkers (int): + Walkers per MPI process. + max_time (int): + Wall-time limit per sub-run (seconds). + polynomial_order (int): + Polynomial order for the a^2->0 extrapolation (default: 2). + num_gfmc_bin_blocks (int): + Binning blocks for post-processing. + num_gfmc_warmup_steps (int): + Warmup steps to discard. + num_gfmc_collect_steps (int): + Weight-collection steps. + time_projection_tau (float, optional): + Imaginary time step for GFMC_t mode (default 0.10). Ignored + when *target_survived_walkers_ratio* or + *num_projection_per_measurement* is set. + target_survived_walkers_ratio (float, optional): + Target survived-walkers ratio (default *None*). Each ``alat`` + independently runs a calibration pilot (``_pilot_a``) to + find its own optimal ``num_projection_per_measurement``. + Set to *None* to disable auto-calibration (requires explicit + *num_projection_per_measurement*). Activates GFMC_n mode. + num_projection_per_measurement (int, optional): + GFMC projections per measurement. When given explicitly, + automatic calibration is disabled and this value is used + for every ``alat``. Activates GFMC_n mode. + non_local_move (str, optional): + Non-local move treatment. Default from ``jqmc_miscs``. + E_scf (float, optional): + Initial energy guess for the GFMC shift (GFMC_n only). + Default from ``jqmc_miscs``. + atomic_force (bool, optional): + Compute atomic forces. Default from ``jqmc_miscs``. + use_swct (bool, optional): + Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. + Default is False for LRDMC. + epsilon_PW (float, optional): + Pathak-Wagner regularization parameter (Bohr). When > 0, + the force estimator is regularized near the nodal surface. + Default from ``jqmc_miscs``. + mcmc_seed (int, optional): + Random seed for MCMC. Default from ``jqmc_miscs``. + verbosity (str, optional): + Verbosity level. Default from ``jqmc_miscs``. + poll_interval (int): + Seconds between job-status polls. + target_error (float): + Target statistical error (Ha) for each sub-LRDMC run. + Passed through to each :class:`LRDMC_Workflow`. + pilot_steps (int): + Pilot measurement steps for target-error estimation. + num_gfmc_projections (int, optional): + Fixed number of measurement steps per production run. + When set, the error-bar pilot is skipped for each sub-LRDMC + and all ``max_continuation`` runs are executed unconditionally. + Passed through to each :class:`LRDMC_Workflow`. + Default *None* (automatic mode). + max_continuation (int): + Maximum number of production runs per sub-LRDMC. + cleanup_patterns (list[str], optional): + Glob patterns for files to delete after successful completion + (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). Local files + are always removed; remote files are removed only when the + workflow targets a remote machine. Passed through to each + child :class:`LRDMC_Workflow`. Default *None* (no cleanup). Examples: - -------- - GFMC_t mode (default):: - - wf = LRDMC_Ext_Workflow( - server_machine_name="cluster", - alat_list=[0.5, 0.4, 0.3], - target_error=0.001, - number_of_walkers=8, - ) - status, files, values = wf.launch() - print(values["extrapolated_energy"], - values["extrapolated_energy_error"]) - - GFMC_n mode with calibration:: - - wf = LRDMC_Ext_Workflow( - server_machine_name="cluster", - alat_list=[0.5, 0.4, 0.3], - target_survived_walkers_ratio=0.97, - target_error=0.001, - number_of_walkers=8, - ) + GFMC_t mode (default):: + + wf = LRDMC_Ext_Workflow( + server_machine_name="cluster", + alat_list=[0.5, 0.4, 0.3], + target_error=0.001, + number_of_walkers=8, + ) + status, files, values = wf.launch() + print(values["extrapolated_energy"], + values["extrapolated_energy_error"]) - As part of a :class:`Launcher` pipeline:: + GFMC_n mode with calibration:: - enc = Container( - label="lrdmc-ext", - dirname="03_lrdmc", - input_files=[FileFrom("mcmc-run", "hamiltonian_data.h5")], - workflow=LRDMC_Ext_Workflow( + wf = LRDMC_Ext_Workflow( server_machine_name="cluster", alat_list=[0.5, 0.4, 0.3], + target_survived_walkers_ratio=0.97, target_error=0.001, - ), - ) + number_of_walkers=8, + ) + + As part of a :class:`Launcher` pipeline:: + + enc = Container( + label="lrdmc-ext", + dirname="03_lrdmc", + input_files=[FileFrom("mcmc-run", "hamiltonian_data.h5")], + workflow=LRDMC_Ext_Workflow( + server_machine_name="cluster", + alat_list=[0.5, 0.4, 0.3], + target_error=0.001, + ), + ) - Output Values - ------------- - After ``launch()`` completes, ``output_values`` may contain: - - extrapolated_energy : float - Continuum-limit (a^2->0) extrapolated energy (Ha). - extrapolated_energy_error : float - Statistical error on ``extrapolated_energy`` (Ha). - per_alat_results : dict - Per-alat energy/error results keyed by ``alat``. - errors : list[str] - Error messages for alat runs that failed. - error : str - Top-level error message (only on failure). + Output Values: + extrapolated_energy (float): + Continuum-limit (a^2->0) extrapolated energy (Ha). + extrapolated_energy_error (float): + Statistical error on ``extrapolated_energy`` (Ha). + per_alat_results (dict): + Per-alat energy/error results keyed by ``alat``. + errors (list[str]): + Error messages for alat runs that failed. + error (str): + Top-level error message (only on failure). Notes: - ----- - * At least two ``alat`` values are required for extrapolation. - With a single value, per-alat results are returned but no - extrapolation is performed. - * Each sub-run directory is named ``lrdmc_alat_/``. + * At least two ``alat`` values are required for extrapolation. + With a single value, per-alat results are returned but no + extrapolation is performed. + * Each sub-run directory is named ``lrdmc_alat_/``. See Also: - -------- - LRDMC_Workflow : Single-alat LRDMC run. + LRDMC_Workflow : Single-alat LRDMC run. """ def __init__( @@ -323,14 +316,12 @@ def __init__( def _make_lrdmc_workflow(self, alat): """Create one :class:`Container` for a given *alat* value. - Parameters - ---------- - alat : float - Lattice spacing. + Args: + alat (float): + Lattice spacing. Returns: - ------- - Container + Container: """ label = f"lrdmc-a{alat:.3f}" dirname = f"lrdmc_alat_{alat:.3f}" @@ -401,9 +392,8 @@ async def run(self) -> tuple: and production phase. Returns: - ------- - tuple - ``(status, output_files, output_values)`` + tuple: + ``(status, output_files, output_values)`` """ self._ensure_project_dir() _wd = self.project_dir @@ -491,9 +481,8 @@ def _extrapolate_energy(self, restart_chks: list[str]): """Run ``jqmc-tool lrdmc extrapolate-energy``. Returns: - ------- - tuple - ``(energy, error)`` or ``(None, None)``. + tuple: + ``(energy, error)`` or ``(None, None)``. """ chk_args = " ".join(restart_chks) cmd = ( diff --git a/jqmc_workflow/lrdmc_workflow.py b/jqmc_workflow/lrdmc_workflow.py index bf3dc366..c029d4e0 100644 --- a/jqmc_workflow/lrdmc_workflow.py +++ b/jqmc_workflow/lrdmc_workflow.py @@ -129,166 +129,159 @@ class LRDMC_Workflow(Workflow): measurement steps, and ``max_continuation`` runs are executed unconditionally. - Parameters - ---------- - server_machine_name : str - Target machine name. - alat : float - Lattice discretization parameter (bohr). - hamiltonian_file : str - Input ``hamiltonian_data.h5``. - queue_label : str - Queue/partition label. - jobname : str - Scheduler job name. - number_of_walkers : int - Walkers per MPI process. - max_time : int - Wall-time limit (seconds). - num_gfmc_bin_blocks : int - Binning blocks for post-processing. - num_gfmc_warmup_steps : int - Warmup steps to discard in post-processing. - num_gfmc_collect_steps : int - Weight-collection steps for energy post-processing. - time_projection_tau : float, optional - Imaginary time step between projections (bohr) for GFMC_t - mode. Default ``0.10``. Ignored when - *target_survived_walkers_ratio* or *num_projection_per_measurement* - is set. - target_survived_walkers_ratio : float, optional - Target survived-walkers ratio for automatic - ``num_projection_per_measurement`` calibration. Setting this - activates GFMC_n mode. The pilot phase runs three short - calculations at ``Ne*k*(0.3/alat)^2`` projections (k=2,4,6), - fits a linear model to the observed survived-walkers ratio, - and picks the value that achieves this target. - num_projection_per_measurement : int, optional - GFMC projections per measurement (GFMC_n mode). When given - explicitly, the automatic calibration is skipped. - non_local_move : str, optional - Non-local move treatment (``"tmove"`` or ``"dltmove"``). Default from ``jqmc_miscs``. - E_scf : float, optional - Initial energy guess for the GFMC shift (GFMC_n only). - Default from ``jqmc_miscs``. - atomic_force : bool, optional - Compute atomic forces. Default from ``jqmc_miscs``. - use_swct : bool, optional - Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. - Default is False for LRDMC. - epsilon_PW : float, optional - Pathak-Wagner regularization parameter (Bohr). When > 0, - the force estimator is regularized near the nodal surface. - Default from ``jqmc_miscs``. - mcmc_seed : int, optional - Random seed for MCMC. Default from ``jqmc_miscs``. - verbosity : str, optional - Verbosity level. Default from ``jqmc_miscs``. - poll_interval : int - Seconds between job-status polls. - target_error : float - Target statistical error (Ha). - pilot_steps : int - Measurement steps for the pilot estimation run. - num_gfmc_projections : int, optional - Fixed number of measurement steps per production run. - When set, the error-bar pilot (``_pilot_b``) is skipped, - ``target_error`` is ignored, and all ``max_continuation`` - production runs are executed unconditionally. Calibration - (``_pilot_a``) still runs when needed (GFMC_n mode with - ``target_survived_walkers_ratio``). Default *None* - (automatic mode). - pilot_queue_label : str, optional - Queue label for the pilot run. Defaults to *queue_label*. - Use a shorter/smaller queue for the pilot to save resources. - max_continuation : int - Maximum number of production runs after the pilot. - cleanup_patterns : list[str], optional - Glob patterns for files to delete after successful completion - (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). Local files - are always removed; remote files are removed only when the - workflow targets a remote machine. Default *None* (no cleanup). + Args: + server_machine_name (str): + Target machine name. + alat (float): + Lattice discretization parameter (bohr). + hamiltonian_file (str): + Input ``hamiltonian_data.h5``. + queue_label (str): + Queue/partition label. + jobname (str): + Scheduler job name. + number_of_walkers (int): + Walkers per MPI process. + max_time (int): + Wall-time limit (seconds). + num_gfmc_bin_blocks (int): + Binning blocks for post-processing. + num_gfmc_warmup_steps (int): + Warmup steps to discard in post-processing. + num_gfmc_collect_steps (int): + Weight-collection steps for energy post-processing. + time_projection_tau (float, optional): + Imaginary time step between projections (bohr) for GFMC_t + mode. Default ``0.10``. Ignored when + *target_survived_walkers_ratio* or *num_projection_per_measurement* + is set. + target_survived_walkers_ratio (float, optional): + Target survived-walkers ratio for automatic + ``num_projection_per_measurement`` calibration. Setting this + activates GFMC_n mode. The pilot phase runs three short + calculations at ``Ne*k*(0.3/alat)^2`` projections (k=2,4,6), + fits a linear model to the observed survived-walkers ratio, + and picks the value that achieves this target. + num_projection_per_measurement (int, optional): + GFMC projections per measurement (GFMC_n mode). When given + explicitly, the automatic calibration is skipped. + non_local_move (str, optional): + Non-local move treatment (``"tmove"`` or ``"dltmove"``). Default from ``jqmc_miscs``. + E_scf (float, optional): + Initial energy guess for the GFMC shift (GFMC_n only). + Default from ``jqmc_miscs``. + atomic_force (bool, optional): + Compute atomic forces. Default from ``jqmc_miscs``. + use_swct (bool, optional): + Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. + Default is False for LRDMC. + epsilon_PW (float, optional): + Pathak-Wagner regularization parameter (Bohr). When > 0, + the force estimator is regularized near the nodal surface. + Default from ``jqmc_miscs``. + mcmc_seed (int, optional): + Random seed for MCMC. Default from ``jqmc_miscs``. + verbosity (str, optional): + Verbosity level. Default from ``jqmc_miscs``. + poll_interval (int): + Seconds between job-status polls. + target_error (float): + Target statistical error (Ha). + pilot_steps (int): + Measurement steps for the pilot estimation run. + num_gfmc_projections (int, optional): + Fixed number of measurement steps per production run. + When set, the error-bar pilot (``_pilot_b``) is skipped, + ``target_error`` is ignored, and all ``max_continuation`` + production runs are executed unconditionally. Calibration + (``_pilot_a``) still runs when needed (GFMC_n mode with + ``target_survived_walkers_ratio``). Default *None* + (automatic mode). + pilot_queue_label (str, optional): + Queue label for the pilot run. Defaults to *queue_label*. + Use a shorter/smaller queue for the pilot to save resources. + max_continuation (int): + Maximum number of production runs after the pilot. + cleanup_patterns (list[str], optional): + Glob patterns for files to delete after successful completion + (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). Local files + are always removed; remote files are removed only when the + workflow targets a remote machine. Default *None* (no cleanup). Examples: - -------- - GFMC_t mode (default):: - - wf = LRDMC_Workflow( - server_machine_name="cluster", - alat=0.3, - target_error=0.0005, - number_of_walkers=8, - ) - status, files, values = wf.launch() - print(values["energy"], values["energy_error"]) - - GFMC_n mode with calibration:: + GFMC_t mode (default):: - wf = LRDMC_Workflow( - server_machine_name="cluster", - alat=0.3, - target_error=0.0005, - target_survived_walkers_ratio=0.97, - number_of_walkers=8, - ) + wf = LRDMC_Workflow( + server_machine_name="cluster", + alat=0.3, + target_error=0.0005, + number_of_walkers=8, + ) + status, files, values = wf.launch() + print(values["energy"], values["energy_error"]) - Fixed-step mode (skip error-bar pilot):: + GFMC_n mode with calibration:: - wf = LRDMC_Workflow( - server_machine_name="cluster", - alat=0.3, - num_gfmc_projections=500, - max_continuation=3, - number_of_walkers=8, - ) + wf = LRDMC_Workflow( + server_machine_name="cluster", + alat=0.3, + target_error=0.0005, + target_survived_walkers_ratio=0.97, + number_of_walkers=8, + ) - As part of a :class:`Launcher` pipeline:: + Fixed-step mode (skip error-bar pilot):: - enc = Container( - label="lrdmc-a0.30", - dirname="03_lrdmc", - input_files=[FileFrom("mcmc-run", "hamiltonian_data.h5")], - workflow=LRDMC_Workflow( + wf = LRDMC_Workflow( server_machine_name="cluster", alat=0.3, - target_error=0.001, - ), - ) + num_gfmc_projections=500, + max_continuation=3, + number_of_walkers=8, + ) + + As part of a :class:`Launcher` pipeline:: + + enc = Container( + label="lrdmc-a0.30", + dirname="03_lrdmc", + input_files=[FileFrom("mcmc-run", "hamiltonian_data.h5")], + workflow=LRDMC_Workflow( + server_machine_name="cluster", + alat=0.3, + target_error=0.001, + ), + ) - Output Values - ------------- - After ``launch()`` completes, ``output_values`` may contain: - - energy : float - DMC energy (Ha). - energy_error : float - Statistical error on ``energy`` (Ha). - alat : float - Lattice spacing used for this run. - restart_chk : str - Basename of the restart checkpoint file. - forces : object - Atomic forces (only when ``atomic_force=True``). - estimated_steps : int - Estimated total measurement steps. - num_projection_per_measurement : int - Number of GFMC projections per measurement - (GFMC_n mode only). - time_projection_tau : float - Imaginary-time projection step (GFMC_t mode only). + Output Values: + energy (float): + DMC energy (Ha). + energy_error (float): + Statistical error on ``energy`` (Ha). + alat (float): + Lattice spacing used for this run. + restart_chk (str): + Basename of the restart checkpoint file. + forces (object): + Atomic forces (only when ``atomic_force=True``). + estimated_steps (int): + Estimated total measurement steps. + num_projection_per_measurement (int): + Number of GFMC projections per measurement + (GFMC_n mode only). + time_projection_tau (float): + Imaginary-time projection step (GFMC_t mode only). Notes: - ----- - * For a^2->0 continuum-limit extrapolation, use - :class:`LRDMC_Ext_Workflow` instead. - * The pilot is skipped on re-entrance if an estimation already - exists in ``workflow_state.toml``. + * For a^2->0 continuum-limit extrapolation, use + :class:`LRDMC_Ext_Workflow` instead. + * The pilot is skipped on re-entrance if an estimation already + exists in ``workflow_state.toml``. See Also: - -------- - LRDMC_Ext_Workflow : Multi-alat extrapolation wrapper. - MCMC_Workflow : VMC production sampling (job_type=mcmc). - VMC_Workflow : Wavefunction optimisation (job_type=vmc). + LRDMC_Ext_Workflow : Multi-alat extrapolation wrapper. + MCMC_Workflow : VMC production sampling (job_type=mcmc). + VMC_Workflow : Wavefunction optimisation (job_type=vmc). """ def __init__( @@ -391,19 +384,18 @@ def _generate_input( ): """Generate LRDMC TOML input file. - Parameters - ---------- - num_steps : int - Number of measurement steps. - input_file : str - Output filename. - restart : bool - Whether this is a restart run. - restart_chk : str or None - Restart checkpoint filename. - num_projection_per_measurement : int or None - Override for GFMC projections per measurement (GFMC_n only). - Falls back to ``self.num_projection_per_measurement``. + Args: + num_steps (int): + Number of measurement steps. + input_file (str): + Output filename. + restart (bool): + Whether this is a restart run. + restart_chk (str or None): + Restart checkpoint filename. + num_projection_per_measurement (int or None): + Override for GFMC projections per measurement (GFMC_n only). + Falls back to ``self.num_projection_per_measurement``. """ jt = self.job_type control_ov = resolve_with_defaults( @@ -1214,19 +1206,17 @@ def _compute_energy(self, restart_chk: str, work_dir: str, output_file: str | No Falls back to ``jqmc-tool`` when *output_file* is *None* or when stdout parsing fails. - Parameters - ---------- - restart_chk : str - Checkpoint filename (basename). - work_dir : str - Directory in which to run the command. - output_file : str, optional - Stdout filename (basename) of the ``jqmc`` run. + Args: + restart_chk (str): + Checkpoint filename (basename). + work_dir (str): + Directory in which to run the command. + output_file (str, optional): + Stdout filename (basename) of the ``jqmc`` run. Returns: - ------- - tuple - ``(energy, error)`` or ``(None, None)``. + tuple: + ``(energy, error)`` or ``(None, None)``. """ # Fast path: parse from jqmc stdout if output_file is not None: @@ -1280,21 +1270,19 @@ def _compute_force(self, restart_chk: str, work_dir: str, output_file: str | Non the ``jqmc`` stdout (``Atomic Forces:`` table). Falls back to ``jqmc-tool`` when *output_file* is *None* or parsing fails. - Parameters - ---------- - restart_chk : str - Checkpoint filename (basename). - work_dir : str - Directory in which to run the command. - output_file : str, optional - Stdout filename (basename) of the ``jqmc`` run. + Args: + restart_chk (str): + Checkpoint filename (basename). + work_dir (str): + Directory in which to run the command. + output_file (str, optional): + Stdout filename (basename) of the ``jqmc`` run. Returns: - ------- - list of dict or None - Each dict has keys ``label``, ``Fx``, ``Fx_err``, - ``Fy``, ``Fy_err``, ``Fz``, ``Fz_err``. - Returns *None* on failure. + list of dict or None: + Each dict has keys ``label``, ``Fx``, ``Fx_err``, + ``Fy``, ``Fy_err``, ``Fz``, ``Fz_err``. + Returns *None* on failure. """ # Fast path: parse from jqmc stdout if output_file is not None: diff --git a/jqmc_workflow/mcmc_workflow.py b/jqmc_workflow/mcmc_workflow.py index 6337f44e..598cfa63 100644 --- a/jqmc_workflow/mcmc_workflow.py +++ b/jqmc_workflow/mcmc_workflow.py @@ -94,128 +94,121 @@ class MCMC_Workflow(Workflow): Each production run uses exactly ``num_mcmc_steps`` measurement steps, and ``max_continuation`` runs are executed unconditionally. - Parameters - ---------- - server_machine_name : str - Name of the target machine (configured in ``~/.jqmc_setting/``). - hamiltonian_file : str - Input ``hamiltonian_data.h5``. - queue_label : str - Queue/partition label. - jobname : str - Scheduler job name. - number_of_walkers : int - Walkers per MPI process. - max_time : int - Wall-time limit (seconds). - num_mcmc_bin_blocks : int - Binning blocks for post-processing. - num_mcmc_warmup_steps : int - Warmup steps to discard in post-processing. - Dt : float, optional - MCMC step size (bohr). Default from ``jqmc_miscs``. - epsilon_AS : float, optional - Attacalite-Sorella regularization parameter. Default from ``jqmc_miscs``. - num_mcmc_per_measurement : int, optional - MCMC updates per measurement. Default from ``jqmc_miscs``. - atomic_force : bool, optional - Compute atomic forces. Default from ``jqmc_miscs``. - use_swct : bool, optional - Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. - Default is True for MCMC. - parameter_derivatives : bool, optional - Compute parameter derivatives. Default from ``jqmc_miscs``. - mcmc_seed : int, optional - Random seed for MCMC. Default from ``jqmc_miscs``. - verbosity : str, optional - Verbosity level. Default from ``jqmc_miscs``. - poll_interval : int - Seconds between job-status polls. - target_error : float - Target statistical error (Ha). Ignored when - *num_mcmc_steps* is set. - num_mcmc_steps : int, optional - Fixed number of measurement steps per production run. When - set, the pilot run is skipped and ``target_error`` is ignored; - each of the ``max_continuation`` production runs uses exactly - this many steps. - pilot_steps : int - Measurement steps for the pilot estimation run. Ignored when - *num_mcmc_steps* is set. - pilot_queue_label : str, optional - Queue label for the pilot run. Defaults to *queue_label*. - Use a shorter/smaller queue for the pilot to save resources. - max_continuation : int - Maximum number of production runs after the pilot. - cleanup_patterns : list[str], optional - Glob patterns for files to delete after successful completion - (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). Local files - are always removed; remote files are removed only when the - workflow targets a remote machine. Default *None* (no cleanup). + Args: + server_machine_name (str): + Name of the target machine (configured in ``~/.jqmc_setting/``). + hamiltonian_file (str): + Input ``hamiltonian_data.h5``. + queue_label (str): + Queue/partition label. + jobname (str): + Scheduler job name. + number_of_walkers (int): + Walkers per MPI process. + max_time (int): + Wall-time limit (seconds). + num_mcmc_bin_blocks (int): + Binning blocks for post-processing. + num_mcmc_warmup_steps (int): + Warmup steps to discard in post-processing. + Dt (float, optional): + MCMC step size (bohr). Default from ``jqmc_miscs``. + epsilon_AS (float, optional): + Attacalite-Sorella regularization parameter. Default from ``jqmc_miscs``. + num_mcmc_per_measurement (int, optional): + MCMC updates per measurement. Default from ``jqmc_miscs``. + atomic_force (bool, optional): + Compute atomic forces. Default from ``jqmc_miscs``. + use_swct (bool, optional): + Apply Space Warp Coordinate Transformation (SWCT) to atomic forces. + Default is True for MCMC. + parameter_derivatives (bool, optional): + Compute parameter derivatives. Default from ``jqmc_miscs``. + mcmc_seed (int, optional): + Random seed for MCMC. Default from ``jqmc_miscs``. + verbosity (str, optional): + Verbosity level. Default from ``jqmc_miscs``. + poll_interval (int): + Seconds between job-status polls. + target_error (float): + Target statistical error (Ha). Ignored when + *num_mcmc_steps* is set. + num_mcmc_steps (int, optional): + Fixed number of measurement steps per production run. When + set, the pilot run is skipped and ``target_error`` is ignored; + each of the ``max_continuation`` production runs uses exactly + this many steps. + pilot_steps (int): + Measurement steps for the pilot estimation run. Ignored when + *num_mcmc_steps* is set. + pilot_queue_label (str, optional): + Queue label for the pilot run. Defaults to *queue_label*. + Use a shorter/smaller queue for the pilot to save resources. + max_continuation (int): + Maximum number of production runs after the pilot. + cleanup_patterns (list[str], optional): + Glob patterns for files to delete after successful completion + (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). Local files + are always removed; remote files are removed only when the + workflow targets a remote machine. Default *None* (no cleanup). Examples: - -------- - Standalone launch (automatic mode):: - - wf = MCMC_Workflow( - server_machine_name="cluster", - target_error=0.0005, - pilot_steps=200, - number_of_walkers=8, - ) - status, files, values = wf.launch() - print(values["energy"], values["energy_error"]) - - Fixed-step mode (no pilot, no target_error check):: + Standalone launch (automatic mode):: - wf = MCMC_Workflow( - server_machine_name="cluster", - num_mcmc_steps=5000, - number_of_walkers=8, - max_continuation=3, - ) - status, files, values = wf.launch() + wf = MCMC_Workflow( + server_machine_name="cluster", + target_error=0.0005, + pilot_steps=200, + number_of_walkers=8, + ) + status, files, values = wf.launch() + print(values["energy"], values["energy_error"]) - As part of a :class:`Launcher` pipeline:: + Fixed-step mode (no pilot, no target_error check):: - enc = Container( - label="mcmc", - dirname="02_mcmc", - input_files=[FileFrom("vmc-opt", "hamiltonian_data_opt_step_9.h5")], - rename_input_files=["hamiltonian_data.h5"], - workflow=MCMC_Workflow( + wf = MCMC_Workflow( server_machine_name="cluster", - target_error=0.001, - ), - ) + num_mcmc_steps=5000, + number_of_walkers=8, + max_continuation=3, + ) + status, files, values = wf.launch() + + As part of a :class:`Launcher` pipeline:: + + enc = Container( + label="mcmc", + dirname="02_mcmc", + input_files=[FileFrom("vmc-opt", "hamiltonian_data_opt_step_9.h5")], + rename_input_files=["hamiltonian_data.h5"], + workflow=MCMC_Workflow( + server_machine_name="cluster", + target_error=0.001, + ), + ) - Output Values - ------------- - After ``launch()`` completes, ``output_values`` may contain: - - energy : float - VMC energy (Ha). - energy_error : float - Statistical error on ``energy`` (Ha). - restart_chk : str - Basename of the restart checkpoint file. - forces : object - Atomic forces (only when ``atomic_force=True``). - num_mcmc_steps : int - Estimated total measurement steps (automatic mode). - In fixed-step mode this key is ``estimated_steps``. + Output Values: + energy (float): + VMC energy (Ha). + energy_error (float): + Statistical error on ``energy`` (Ha). + restart_chk (str): + Basename of the restart checkpoint file. + forces (object): + Atomic forces (only when ``atomic_force=True``). + num_mcmc_steps (int): + Estimated total measurement steps (automatic mode). + In fixed-step mode this key is ``estimated_steps``. Notes: - ----- - * The pilot run is skipped on re-entrance if an estimation already - exists in ``workflow_state.toml``. - * Continuation runs restart from the most recent ``.h5`` - checkpoint file. + * The pilot run is skipped on re-entrance if an estimation already + exists in ``workflow_state.toml``. + * Continuation runs restart from the most recent ``.h5`` + checkpoint file. See Also: - -------- - VMC_Workflow : Wavefunction optimisation (job_type=vmc). - LRDMC_Workflow : Diffusion Monte Carlo (job_type=lrdmc-bra / lrdmc-tau). + VMC_Workflow : Wavefunction optimisation (job_type=vmc). + LRDMC_Workflow : Diffusion Monte Carlo (job_type=lrdmc-bra / lrdmc-tau). """ def __init__( @@ -907,19 +900,17 @@ def _compute_energy(self, restart_chk: str, work_dir: str, output_file: str | No Falls back to ``jqmc-tool`` when *output_file* is *None* or when stdout parsing fails. - Parameters - ---------- - restart_chk : str - Checkpoint filename (basename). - work_dir : str - Directory in which to run the command. - output_file : str, optional - Stdout filename (basename) of the ``jqmc`` run. + Args: + restart_chk (str): + Checkpoint filename (basename). + work_dir (str): + Directory in which to run the command. + output_file (str, optional): + Stdout filename (basename) of the ``jqmc`` run. Returns: - ------- - tuple - ``(energy, error)`` or ``(None, None)``. + tuple: + ``(energy, error)`` or ``(None, None)``. """ # Fast path: parse from jqmc stdout if output_file is not None: @@ -968,21 +959,19 @@ def _compute_force(self, restart_chk: str, work_dir: str, output_file: str | Non the ``jqmc`` stdout (``Atomic Forces:`` table). Falls back to ``jqmc-tool`` when *output_file* is *None* or parsing fails. - Parameters - ---------- - restart_chk : str - Checkpoint filename (basename). - work_dir : str - Directory in which to run the command. - output_file : str, optional - Stdout filename (basename) of the ``jqmc`` run. + Args: + restart_chk (str): + Checkpoint filename (basename). + work_dir (str): + Directory in which to run the command. + output_file (str, optional): + Stdout filename (basename) of the ``jqmc`` run. Returns: - ------- - list of dict or None - Each dict has keys ``label``, ``Fx``, ``Fx_err``, - ``Fy``, ``Fy_err``, ``Fz``, ``Fz_err``. - Returns *None* on failure. + list of dict or None: + Each dict has keys ``label``, ``Fx``, ``Fx_err``, + ``Fy``, ``Fy_err``, ``Fz``, ``Fz_err``. + Returns *None* on failure. """ # Fast path: parse from jqmc stdout if output_file is not None: diff --git a/jqmc_workflow/vmc_workflow.py b/jqmc_workflow/vmc_workflow.py index 495a43b1..40bdb7c4 100644 --- a/jqmc_workflow/vmc_workflow.py +++ b/jqmc_workflow/vmc_workflow.py @@ -93,178 +93,171 @@ class VMC_Workflow(Workflow): optimisation step, and ``max_continuation`` runs are executed unconditionally. - Parameters - ---------- - server_machine_name : str - Name of the target machine (must be configured in ``~/.jqmc_setting/``). - num_opt_steps : int - Number of optimization iterations for production runs. - hamiltonian_file : str - Input ``hamiltonian_data.h5``. - queue_label : str - Queue/partition label from ``queue_data.toml``. - jobname : str - Job name for the scheduler. - number_of_walkers : int - Walkers per MPI process. - max_time : int - Wall-time limit in seconds. - Dt : float, optional - MCMC step size (bohr). Default from ``jqmc_miscs``. - epsilon_AS : float, optional - Attacalite-Sorella regularization parameter. Default from ``jqmc_miscs``. - num_mcmc_per_measurement : int, optional - MCMC updates per measurement. Default from ``jqmc_miscs``. - num_mcmc_warmup_steps : int, optional - Warmup measurement steps to discard. Default from ``jqmc_miscs``. - num_mcmc_bin_blocks : int, optional - Binning blocks. Default from ``jqmc_miscs``. - wf_dump_freq : int, optional - Wavefunction dump frequency. Default from ``jqmc_miscs``. - opt_J1_param : bool, optional - Optimize J1 Jastrow parameters. Default from ``jqmc_miscs``. - opt_J2_param : bool, optional - Optimize J2 Jastrow parameters. Default from ``jqmc_miscs``. - opt_J3_param : bool, optional - Optimize J3 Jastrow parameters. Default from ``jqmc_miscs``. - opt_JNN_param : bool, optional - Optimize neural-network Jastrow parameters. Default from ``jqmc_miscs``. - opt_lambda_param : bool, optional - Optimize lambda (geminal) parameters. Default from ``jqmc_miscs``. - opt_with_projected_MOs : bool, optional - Optimize in a restricted MO space. Default from ``jqmc_miscs``. - opt_J3_basis_exp : bool, optional - Optimize J3 AO Gaussian exponents. Default from ``jqmc_miscs``. - opt_J3_basis_coeff : bool, optional - Optimize J3 AO contraction coefficients. Default from ``jqmc_miscs``. - opt_lambda_basis_exp : bool, optional - Optimize Geminal AO Gaussian exponents. Default from ``jqmc_miscs``. - opt_lambda_basis_coeff : bool, optional - Optimize Geminal AO contraction coefficients. Default from ``jqmc_miscs``. - optimizer_kwargs : dict, optional - Optimizer configuration dict. Default from ``jqmc_miscs``. - mcmc_seed : int, optional - Random seed for MCMC. Default from ``jqmc_miscs``. - verbosity : str, optional - Verbosity level. Default from ``jqmc_miscs``. - poll_interval : int - Seconds between job-status polls. - target_error : float - Target statistical error (Ha) per optimization step. Ignored - when *num_mcmc_steps* is set. - num_mcmc_steps : int, optional - Fixed number of MCMC measurement steps per optimisation step. - When set, the pilot run is skipped and ``target_error`` is - ignored; each of the ``max_continuation`` production runs uses - exactly this many MCMC steps per opt step. - pilot_mcmc_steps : int - MCMC steps per opt-step for the pilot run. Ignored when - *num_mcmc_steps* is set. - pilot_vmc_steps : int - Number of optimization steps in the pilot run (small; just - enough to estimate the error bar). - pilot_queue_label : str, optional - Queue label for the pilot run. Defaults to *queue_label*. - Use a shorter/smaller queue for the pilot to save resources. - max_continuation : int - Maximum number of production runs after the pilot. - target_snr : float - Target signal-to-noise ratio ``max(|f|/|std f|)`` for force - convergence. The workflow continues until the averaged - S/N drops to or below this threshold. - snr_avg_window : int - Number of trailing optimization steps over which to average - the signal-to-noise ratio for the convergence check. - When there are fewer S/N values than this window, all - available values are averaged. - cleanup_patterns : list[str], optional - Glob patterns for files to delete after successful completion - (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). Local files - are always removed; remote files are removed only when the - workflow targets a remote machine. Default *None* (no cleanup). + Args: + server_machine_name (str): + Name of the target machine (must be configured in ``~/.jqmc_setting/``). + num_opt_steps (int): + Number of optimization iterations for production runs. + hamiltonian_file (str): + Input ``hamiltonian_data.h5``. + queue_label (str): + Queue/partition label from ``queue_data.toml``. + jobname (str): + Job name for the scheduler. + number_of_walkers (int): + Walkers per MPI process. + max_time (int): + Wall-time limit in seconds. + Dt (float, optional): + MCMC step size (bohr). Default from ``jqmc_miscs``. + epsilon_AS (float, optional): + Attacalite-Sorella regularization parameter. Default from ``jqmc_miscs``. + num_mcmc_per_measurement (int, optional): + MCMC updates per measurement. Default from ``jqmc_miscs``. + num_mcmc_warmup_steps (int, optional): + Warmup measurement steps to discard. Default from ``jqmc_miscs``. + num_mcmc_bin_blocks (int, optional): + Binning blocks. Default from ``jqmc_miscs``. + wf_dump_freq (int, optional): + Wavefunction dump frequency. Default from ``jqmc_miscs``. + opt_J1_param (bool, optional): + Optimize J1 Jastrow parameters. Default from ``jqmc_miscs``. + opt_J2_param (bool, optional): + Optimize J2 Jastrow parameters. Default from ``jqmc_miscs``. + opt_J3_param (bool, optional): + Optimize J3 Jastrow parameters. Default from ``jqmc_miscs``. + opt_JNN_param (bool, optional): + Optimize neural-network Jastrow parameters. Default from ``jqmc_miscs``. + opt_lambda_param (bool, optional): + Optimize lambda (geminal) parameters. Default from ``jqmc_miscs``. + opt_with_projected_MOs (bool, optional): + Optimize in a restricted MO space. Default from ``jqmc_miscs``. + opt_J3_basis_exp (bool, optional): + Optimize J3 AO Gaussian exponents. Default from ``jqmc_miscs``. + opt_J3_basis_coeff (bool, optional): + Optimize J3 AO contraction coefficients. Default from ``jqmc_miscs``. + opt_lambda_basis_exp (bool, optional): + Optimize Geminal AO Gaussian exponents. Default from ``jqmc_miscs``. + opt_lambda_basis_coeff (bool, optional): + Optimize Geminal AO contraction coefficients. Default from ``jqmc_miscs``. + optimizer_kwargs (dict, optional): + Optimizer configuration dict. Default from ``jqmc_miscs``. + mcmc_seed (int, optional): + Random seed for MCMC. Default from ``jqmc_miscs``. + verbosity (str, optional): + Verbosity level. Default from ``jqmc_miscs``. + poll_interval (int): + Seconds between job-status polls. + target_error (float): + Target statistical error (Ha) per optimization step. Ignored + when *num_mcmc_steps* is set. + num_mcmc_steps (int, optional): + Fixed number of MCMC measurement steps per optimisation step. + When set, the pilot run is skipped and ``target_error`` is + ignored; each of the ``max_continuation`` production runs uses + exactly this many MCMC steps per opt step. + pilot_mcmc_steps (int): + MCMC steps per opt-step for the pilot run. Ignored when + *num_mcmc_steps* is set. + pilot_vmc_steps (int): + Number of optimization steps in the pilot run (small; just + enough to estimate the error bar). + pilot_queue_label (str, optional): + Queue label for the pilot run. Defaults to *queue_label*. + Use a shorter/smaller queue for the pilot to save resources. + max_continuation (int): + Maximum number of production runs after the pilot. + target_snr (float): + Target signal-to-noise ratio ``max(|f|/|std f|)`` for force + convergence. The workflow continues until the averaged + S/N drops to or below this threshold. + snr_avg_window (int): + Number of trailing optimization steps over which to average + the signal-to-noise ratio for the convergence check. + When there are fewer S/N values than this window, all + available values are averaged. + cleanup_patterns (list[str], optional): + Glob patterns for files to delete after successful completion + (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). Local files + are always removed; remote files are removed only when the + workflow targets a remote machine. Default *None* (no cleanup). Examples: - -------- - Standalone launch (automatic mode):: - - wf = VMC_Workflow( - server_machine_name="cluster", - num_opt_steps=20, - target_error=0.001, - pilot_mcmc_steps=50, - pilot_vmc_steps=5, - number_of_walkers=8, - ) - status, files, values = wf.launch() - print(values["optimized_hamiltonian"]) - - Fixed-step mode (no pilot, no target_error check):: + Standalone launch (automatic mode):: - wf = VMC_Workflow( - server_machine_name="cluster", - num_opt_steps=20, - num_mcmc_steps=500, - number_of_walkers=8, - max_continuation=3, - ) - status, files, values = wf.launch() + wf = VMC_Workflow( + server_machine_name="cluster", + num_opt_steps=20, + target_error=0.001, + pilot_mcmc_steps=50, + pilot_vmc_steps=5, + number_of_walkers=8, + ) + status, files, values = wf.launch() + print(values["optimized_hamiltonian"]) - As part of a :class:`Launcher` pipeline:: + Fixed-step mode (no pilot, no target_error check):: - enc = Container( - label="vmc", - dirname="01_vmc", - input_files=[FileFrom("wf", "hamiltonian_data.h5")], - workflow=VMC_Workflow( + wf = VMC_Workflow( server_machine_name="cluster", num_opt_steps=20, - target_error=0.001, - ), - ) + num_mcmc_steps=500, + number_of_walkers=8, + max_continuation=3, + ) + status, files, values = wf.launch() + + As part of a :class:`Launcher` pipeline:: + + enc = Container( + label="vmc", + dirname="01_vmc", + input_files=[FileFrom("wf", "hamiltonian_data.h5")], + workflow=VMC_Workflow( + server_machine_name="cluster", + num_opt_steps=20, + target_error=0.001, + ), + ) - Output Values - ------------- - After ``launch()`` completes, ``output_values`` may contain: - - optimized_hamiltonian : str - Basename of the last optimised Hamiltonian file - (e.g. ``"hamiltonian_data_opt_step_91.h5"``). - Use with ``ValueFrom("vmc", "optimized_hamiltonian")`` - to pass the filename dynamically to downstream workflows. - checkpoint : str - Basename of the restart checkpoint file. - num_mcmc_steps : int - Estimated MCMC steps per optimisation step - (automatic mode). In fixed-step mode this key is - ``estimated_mcmc_steps`` instead. - energy : float - Energy from the last optimisation step (Ha). - energy_error : float - Statistical error on ``energy`` (Ha). - signal_to_noise : float - Average signal-to-noise ratio over the trailing window - (only when force-convergence is enabled). - signal_to_noise_last : float - Signal-to-noise ratio of the last optimisation step. - energy_slope : float - Slope of energy vs. step from the trailing window - (only when ``energy_slope_sigma_threshold`` is set). - energy_slope_std : float - Standard deviation of the energy slope. + Output Values: + optimized_hamiltonian (str): + Basename of the last optimised Hamiltonian file + (e.g. ``"hamiltonian_data_opt_step_91.h5"``). + Use with ``ValueFrom("vmc", "optimized_hamiltonian")`` + to pass the filename dynamically to downstream workflows. + checkpoint (str): + Basename of the restart checkpoint file. + num_mcmc_steps (int): + Estimated MCMC steps per optimisation step + (automatic mode). In fixed-step mode this key is + ``estimated_mcmc_steps`` instead. + energy (float): + Energy from the last optimisation step (Ha). + energy_error (float): + Statistical error on ``energy`` (Ha). + signal_to_noise (float): + Average signal-to-noise ratio over the trailing window + (only when force-convergence is enabled). + signal_to_noise_last (float): + Signal-to-noise ratio of the last optimisation step. + energy_slope (float): + Slope of energy vs. step from the trailing window + (only when ``energy_slope_sigma_threshold`` is set). + energy_slope_std (float): + Standard deviation of the energy slope. Notes: - ----- - * The pilot uses a small number of opt steps (``pilot_vmc_steps``) - just to estimate the error. The real optimisation happens in - production runs with the full ``num_opt_steps``. - * The estimation is stored in ``workflow_state.toml`` under - ``[estimation]``; on re-entrance the pilot is skipped. + * The pilot uses a small number of opt steps (``pilot_vmc_steps``) + just to estimate the error. The real optimisation happens in + production runs with the full ``num_opt_steps``. + * The estimation is stored in ``workflow_state.toml`` under + ``[estimation]``; on re-entrance the pilot is skipped. See Also: - -------- - MCMC_Workflow : VMC production sampling (job_type=mcmc). - LRDMC_Workflow : Diffusion Monte Carlo (job_type=lrdmc-bra / lrdmc-tau). - WF_Workflow : TREXIO -> hamiltonian_data conversion. + MCMC_Workflow : VMC production sampling (job_type=mcmc). + LRDMC_Workflow : Diffusion Monte Carlo (job_type=lrdmc-bra / lrdmc-tau). + WF_Workflow : TREXIO -> hamiltonian_data conversion. """ def __init__( @@ -369,18 +362,17 @@ def _generate_input( ): """Generate a VMC TOML input file. - Parameters - ---------- - num_mcmc_steps : int - MCMC measurement steps per optimization step. - num_opt_steps : int - Number of optimization iterations. - input_file : str - Output filename for the TOML. - restart : bool - Whether to restart from a checkpoint. - restart_chk : str, optional - Checkpoint file to restart from. + Args: + num_mcmc_steps (int): + MCMC measurement steps per optimization step. + num_opt_steps (int): + Number of optimization iterations. + input_file (str): + Output filename for the TOML. + restart (bool): + Whether to restart from a checkpoint. + restart_chk (str, optional): + Checkpoint file to restart from. """ control_ov = resolve_with_defaults( "control", @@ -995,11 +987,10 @@ def _parse_all_snr(output_file): """Parse all signal-to-noise ratios from a VMC output file. Returns: - ------- - list[float] - All ``max(|f|/|std f|)`` values in order, one per - optimization step. Empty list if the file is missing - or contains no S/N lines. + list[float]: + All ``max(|f|/|std f|)`` values in order, one per + optimization step. Empty list if the file is missing + or contains no S/N lines. """ if not os.path.isfile(output_file): return [] @@ -1024,10 +1015,9 @@ def _parse_all_energies(output_file: str) -> list[tuple[float, float]]: :class:`VMC_Step_Data` and returns the energy/error pairs. Returns: - ------- - list[tuple[float, float]] - ``[(E_1, sigma_1), (E_2, sigma_2), ...]`` in file order. - Empty list if the file is missing or unparseable. + list[tuple[float, float]]: + ``[(E_1, sigma_1), (E_2, sigma_2), ...]`` in file order. + Empty list if the file is missing or unparseable. """ if not os.path.isfile(output_file): return [] @@ -1050,19 +1040,17 @@ def _fit_energy_slope( Model: ``E_k = a + b * k + eps_k``, weight ``w_k = 1 / sigma_k^2``. - Parameters - ---------- - energies : list[float] - Energy value per optimisation step (length *N* >= 2). - energy_errors : list[float] - Statistical error per step (length *N*, positive). + Args: + energies (list[float]): + Energy value per optimisation step (length *N* >= 2). + energy_errors (list[float]): + Statistical error per step (length *N*, positive). Returns: - ------- - slope : float - Weighted least-squares slope *b*. - slope_std : float - Standard error of *b*. + slope (float): + Weighted least-squares slope *b*. + slope_std (float): + Standard error of *b*. """ import numpy as np @@ -1090,9 +1078,8 @@ def _parse_last_opt_energy(output_file): reflects the optimized wavefunction quality. Returns: - ------- - tuple - ``(energy, error)`` or ``(None, None)``. + tuple: + ``(energy, error)`` or ``(None, None)``. """ if not os.path.isfile(output_file): return None, None diff --git a/jqmc_workflow/wf_workflow.py b/jqmc_workflow/wf_workflow.py index 41779883..4590e21d 100644 --- a/jqmc_workflow/wf_workflow.py +++ b/jqmc_workflow/wf_workflow.py @@ -55,57 +55,53 @@ class WF_Workflow(Workflow): Calls ``jqmc-tool trexio convert-to`` under the hood. - Parameters - ---------- - trexio_file : str - Path to the input TREXIO ``.h5`` file. - hamiltonian_file : str - Output filename (default: ``"hamiltonian_data.h5"``). - j1_parameter : float, optional - Jastrow one-body parameter (``-j1``). - j1_type : str, optional - Jastrow one-body functional form (``--jastrow-1b-type``). - ``"exp"`` (default) or ``"pade"``. - j2_parameter : float, optional - Jastrow two-body parameter (``-j2``). - j2_type : str, optional - Jastrow two-body functional form (``--jastrow-2b-type``). - ``"pade"`` (default) or ``"exp"``. - j3_basis_type : str, optional - Jastrow three-body basis-set type (``-j3``). - One of ``"ao"``, ``"ao-full"``, ``"ao-small"``, ``"ao-medium"``, - ``"ao-large"``, ``"mo"``, ``"none"``, or ``None`` (disabled). - j_nn_type : str, optional - Neural-network Jastrow type (``-j-nn-type``), e.g. ``"schnet"``. - j_nn_params : list[str], optional - Extra NN Jastrow parameters (``-jp key=value``). - ao_conv_to : str, optional - Convert AOs after building the Hamiltonian (``--ao-conv-to``). - ``"cart"`` -> convert to Cartesian AOs, - ``"sphe"`` -> convert to spherical-harmonic AOs, - ``None`` -> keep the original representation. - - Example: - ------- - >>> wf = WF_Workflow( - ... trexio_file="molecular.h5", - ... j1_parameter=1.0, - ... j1_type="pade", - ... j2_parameter=0.5, - ... j2_type="exp", - ... j3_basis_type="ao-small", - ... ) - >>> status, out_files, out_values = wf.launch() + Args: + trexio_file (str): + Path to the input TREXIO ``.h5`` file. + hamiltonian_file (str): + Output filename (default: ``"hamiltonian_data.h5"``). + j1_parameter (float, optional): + Jastrow one-body parameter (``-j1``). + j1_type (str, optional): + Jastrow one-body functional form (``--jastrow-1b-type``). + ``"exp"`` (default) or ``"pade"``. + j2_parameter (float, optional): + Jastrow two-body parameter (``-j2``). + j2_type (str, optional): + Jastrow two-body functional form (``--jastrow-2b-type``). + ``"pade"`` (default) or ``"exp"``. + j3_basis_type (str, optional): + Jastrow three-body basis-set type (``-j3``). + One of ``"ao"``, ``"ao-full"``, ``"ao-small"``, ``"ao-medium"``, + ``"ao-large"``, ``"mo"``, ``"none"``, or ``None`` (disabled). + j_nn_type (str, optional): + Neural-network Jastrow type (``-j-nn-type``), e.g. ``"schnet"``. + j_nn_params (list[str], optional): + Extra NN Jastrow parameters (``-jp key=value``). + ao_conv_to (str, optional): + Convert AOs after building the Hamiltonian (``--ao-conv-to``). + ``"cart"`` -> convert to Cartesian AOs, + ``"sphe"`` -> convert to spherical-harmonic AOs, + ``None`` -> keep the original representation. + + Examples: + >>> wf = WF_Workflow( + ... trexio_file="molecular.h5", + ... j1_parameter=1.0, + ... j1_type="pade", + ... j2_parameter=0.5, + ... j2_type="exp", + ... j3_basis_type="ao-small", + ... ) + >>> status, out_files, out_values = wf.launch() Notes: - ----- - This workflow runs **locally** -- no remote job submission is - involved. It calls ``jqmc-tool trexio convert-to`` via - :func:`subprocess.run`. + This workflow runs **locally** -- no remote job submission is + involved. It calls ``jqmc-tool trexio convert-to`` via + :func:`subprocess.run`. See Also: - -------- - VMC_Workflow : Optimise the wavefunction produced by this step. + VMC_Workflow : Optimise the wavefunction produced by this step. """ def __init__( @@ -176,9 +172,8 @@ async def run(self) -> tuple: """Run the TREXIO->hamiltonian conversion (locally). Returns: - ------- - tuple - ``(status, output_files, output_values)`` + tuple: + ``(status, output_files, output_values)`` """ self._ensure_project_dir() _wd = self.project_dir diff --git a/jqmc_workflow/workflow.py b/jqmc_workflow/workflow.py index f040a6f6..4cbedcc4 100644 --- a/jqmc_workflow/workflow.py +++ b/jqmc_workflow/workflow.py @@ -79,46 +79,43 @@ class FileFrom: inter-workflow file dependencies. The :class:`Launcher` resolves these placeholders before a workflow is launched. - Parameters - ---------- - label : str - Label of the upstream workflow that produces the file. - filename : str or ValueFrom - Filename (basename) to pull from the upstream workflow's - output directory. Can be a plain string when the filename is - known at definition time, or a :class:`ValueFrom` for names - that are only determined at runtime (e.g. the optimised - Hamiltonian whose step number depends on convergence). + Args: + label (str): + Label of the upstream workflow that produces the file. + filename (str or ValueFrom): + Filename (basename) to pull from the upstream workflow's + output directory. Can be a plain string when the filename is + known at definition time, or a :class:`ValueFrom` for names + that are only determined at runtime (e.g. the optimised + Hamiltonian whose step number depends on convergence). Examples: - -------- - Static filename (step number known in advance):: - - Container( - label="mcmc-run", - dirname="mcmc", - input_files=[FileFrom("vmc-opt", "hamiltonian_data_opt_step_9.h5")], - rename_input_files=["hamiltonian_data.h5"], - workflow=MCMC_Workflow(...), - ) + Static filename (step number known in advance):: + + Container( + label="mcmc-run", + dirname="mcmc", + input_files=[FileFrom("vmc-opt", "hamiltonian_data_opt_step_9.h5")], + rename_input_files=["hamiltonian_data.h5"], + workflow=MCMC_Workflow(...), + ) - Dynamic filename (resolved from upstream ``output_values``):: - - Container( - label="mcmc-run", - dirname="mcmc", - input_files=[ - FileFrom("vmc-opt", - ValueFrom("vmc-opt", "optimized_hamiltonian")), - ], - rename_input_files=["hamiltonian_data.h5"], - workflow=MCMC_Workflow(...), - ) + Dynamic filename (resolved from upstream ``output_values``):: + + Container( + label="mcmc-run", + dirname="mcmc", + input_files=[ + FileFrom("vmc-opt", + ValueFrom("vmc-opt", "optimized_hamiltonian")), + ], + rename_input_files=["hamiltonian_data.h5"], + workflow=MCMC_Workflow(...), + ) See Also: - -------- - ValueFrom : Declare a scalar-value dependency. - Launcher : Resolves ``FileFrom`` / ``ValueFrom`` at launch time. + ValueFrom : Declare a scalar-value dependency. + Launcher : Resolves ``FileFrom`` / ``ValueFrom`` at launch time. """ def __init__(self, label: str, filename): @@ -136,41 +133,38 @@ class ValueFrom: error, filename string, etc.) produced by an upstream workflow. The :class:`Launcher` resolves these placeholders before launch. - Parameters - ---------- - label : str - Label of the upstream workflow that produces the value. - key : str - Key name in the upstream workflow's ``output_values`` dict. - See the *Output Values* section of each workflow class for - available keys: - - * :class:`VMC_Workflow` -- ``optimized_hamiltonian``, - ``energy``, ``energy_error``, ``checkpoint``, ... - * :class:`MCMC_Workflow` -- ``energy``, ``energy_error``, - ``restart_chk``, ``forces``, ... - * :class:`LRDMC_Workflow` -- ``energy``, ``energy_error``, - ``alat``, ``restart_chk``, ``forces``, ... - * :class:`LRDMC_Ext_Workflow` -- ``extrapolated_energy``, - ``extrapolated_energy_error``, ``per_alat_results``, ... + Args: + label (str): + Label of the upstream workflow that produces the value. + key (str): + Key name in the upstream workflow's ``output_values`` dict. + See the *Output Values* section of each workflow class for + available keys: + + * :class:`VMC_Workflow` -- ``optimized_hamiltonian``, + ``energy``, ``energy_error``, ``checkpoint``, ... + * :class:`MCMC_Workflow` -- ``energy``, ``energy_error``, + ``restart_chk``, ``forces``, ... + * :class:`LRDMC_Workflow` -- ``energy``, ``energy_error``, + ``alat``, ``restart_chk``, ``forces``, ... + * :class:`LRDMC_Ext_Workflow` -- ``extrapolated_energy``, + ``extrapolated_energy_error``, ``per_alat_results``, ... Examples: - -------- - Feed the MCMC energy into an LRDMC workflow as ``trial_energy``:: + Feed the MCMC energy into an LRDMC workflow as ``trial_energy``:: - LRDMC_Workflow( - trial_energy=ValueFrom("mcmc-run", "energy"), - ... - ) + LRDMC_Workflow( + trial_energy=ValueFrom("mcmc-run", "energy"), + ... + ) - Pass the VMC-optimised Hamiltonian dynamically via ``FileFrom``:: + Pass the VMC-optimised Hamiltonian dynamically via ``FileFrom``:: - FileFrom("vmc-opt", ValueFrom("vmc-opt", "optimized_hamiltonian")) + FileFrom("vmc-opt", ValueFrom("vmc-opt", "optimized_hamiltonian")) See Also: - -------- - FileFrom : Declare a file dependency. - Launcher : Resolves ``FileFrom`` / ``ValueFrom`` at launch time. + FileFrom : Declare a file dependency. + Launcher : Resolves ``FileFrom`` / ``ValueFrom`` at launch time. """ def __init__(self, label: str, key: str): @@ -197,56 +191,52 @@ class Workflow: Every concrete workflow (VMC, MCMC, LRDMC, WF, ...) inherits from this class and overrides :meth:`configure` and :meth:`run`. - Parameters - ---------- - project_dir : str, optional - Absolute path to the working directory for this workflow. - When *None* (the default), ``project_dir`` is set to the - process CWD at the time :meth:`run` is first called. - :class:`Container` sets this explicitly before launching the - inner workflow. - cleanup_patterns : list[str], optional - Glob patterns for files to delete after the workflow completes - successfully (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). - Local files matching the patterns are always removed. Remote - files are removed only when the workflow targets a remote - machine. Default is *None* (empty list -- no cleanup). + Args: + project_dir (str, optional): + Absolute path to the working directory for this workflow. + When *None* (the default), ``project_dir`` is set to the + process CWD at the time :meth:`run` is first called. + :class:`Container` sets this explicitly before launching the + inner workflow. + cleanup_patterns (list[str], optional): + Glob patterns for files to delete after the workflow completes + successfully (e.g. ``["restart.h5", "hamiltonian_opt*.h5"]``). + Local files matching the patterns are always removed. Remote + files are removed only when the workflow targets a remote + machine. Default is *None* (empty list -- no cleanup). Attributes: - ---------- - status : WorkflowStatus - Current lifecycle status. - phase : ScientificPhase - Current scientific phase. - output_files : list[str] - Filenames produced by the workflow (populated after run). - output_values : dict - Scalar results (energy, error, ...) produced by the workflow. - project_dir : str or None - Working directory for file I/O. Resolved to an absolute path. - cleanup_patterns : list[str] - Glob patterns for post-completion file cleanup. + status (WorkflowStatus): + Current lifecycle status. + phase (ScientificPhase): + Current scientific phase. + output_files (list[str]): + Filenames produced by the workflow (populated after run). + output_values (dict): + Scalar results (energy, error, ...) produced by the workflow. + project_dir (str or None): + Working directory for file I/O. Resolved to an absolute path. + cleanup_patterns (list[str]): + Glob patterns for post-completion file cleanup. Notes: - ----- - **Subclass contract:** + **Subclass contract:** - * Override :meth:`configure` and :meth:`run`, return - ``(status, output_files, output_values)`` from ``run()``. - * Call ``super().__init__()`` in your constructor. + * Override :meth:`configure` and :meth:`run`, return + ``(status, output_files, output_values)`` from ``run()``. + * Call ``super().__init__()`` in your constructor. Examples: - -------- - Minimal custom workflow:: + Minimal custom workflow:: - class MyWorkflow(Workflow): - def configure(self): - return {"param": 42} + class MyWorkflow(Workflow): + def configure(self): + return {"param": 42} - async def run(self): - # ... do work ... - self.status = WorkflowStatus.COMPLETED - return self.status, ["result.h5"], {"energy": -1.23} + async def run(self): + # ... do work ... + self.status = WorkflowStatus.COMPLETED + return self.status, ["result.h5"], {"energy": -1.23} """ def __init__(self, project_dir: str | None = None, cleanup_patterns: list[str] | None = None): @@ -364,23 +354,20 @@ def launch(self): async def async_submit(self, action: str = "run") -> dict: """Start the workflow in the background and return tracking info. - Parameters - ---------- - action : str - MCP tool name (e.g. ``"run_vmc"``). Checked against - :func:`allowed_actions` for the current phase and status. + Args: + action (str): + MCP tool name (e.g. ``"run_vmc"``). Checked against + :func:`allowed_actions` for the current phase and status. Returns: - ------- - dict - ``{"status": "submitted", "project_dir": ...}``. + dict: + ``{"status": "submitted", "project_dir": ...}``. Raises: - ------ - ValueError - If *action* is not allowed in the current phase/status. - RuntimeError - If the workflow has already been submitted. + ValueError: + If *action* is not allowed in the current phase/status. + RuntimeError: + If the workflow has already been submitted. """ if self._bg_task is not None and not self._bg_task.done(): raise RuntimeError("Workflow already submitted and still running.") @@ -394,10 +381,9 @@ async def async_poll(self) -> dict: """Check whether the submitted workflow has completed. Returns: - ------- - dict - Status dict. Includes ``get_workflow_summary()`` when - the task is still running. + dict: + Status dict. Includes ``get_workflow_summary()`` when + the task is still running. """ if self._bg_task is None: return {"status": "not_submitted"} @@ -412,16 +398,14 @@ async def async_collect(self) -> dict: """Collect results from the completed workflow. Returns: - ------- - dict - ``{"status": ..., "output_files": [...], **output_values}``. + dict: + ``{"status": ..., "output_files": [...], **output_values}``. Raises: - ------ - RuntimeError - If the workflow was not submitted or is still running. - Exception - Re-raises the original exception if the workflow failed. + RuntimeError: + If the workflow was not submitted or is still running. + Exception: + Re-raises the original exception if the workflow failed. """ if self._bg_task is None: raise RuntimeError("No workflow has been submitted. Call async_submit() first.") @@ -484,27 +468,26 @@ async def _submit_and_wait( * ``submitted`` -- resume waiting, then fetch * no record -- submit a new job - Parameters - ---------- - input_file : str - Basename of the input TOML file (relative to *work_dir*). - output_file : str - Basename of the stdout capture file. - work_dir : str - Absolute path to the directory where the job runs. - extra_from_objects : list, optional - Additional files to upload with the job (e.g. checkpoint). - fetch_from_objects : list, optional - Glob patterns for files to fetch. Defaults to - ``["*.h5", output_file]``. - queue_label : str, optional - Override for the queue label. - step : int, optional - Step index (0 for pilot, 1+ for production). Used for - cross-run continuation detection. - run_id : str - Per-job identifier used for scheduler script and stdout/stderr - naming. + Args: + input_file (str): + Basename of the input TOML file (relative to *work_dir*). + output_file (str): + Basename of the stdout capture file. + work_dir (str): + Absolute path to the directory where the job runs. + extra_from_objects (list, optional): + Additional files to upload with the job (e.g. checkpoint). + fetch_from_objects (list, optional): + Glob patterns for files to fetch. Defaults to + ``["*.h5", output_file]``. + queue_label (str, optional): + Override for the queue label. + step (int, optional): + Step index (0 for pilot, 1+ for production). Used for + cross-run continuation detection. + run_id (str): + Per-job identifier used for scheduler script and stdout/stderr + naming. """ if fetch_from_objects is None: fetch_from_objects = ["*.h5", output_file] @@ -644,53 +627,49 @@ class Container: ``completed``, the workflow is *not* re-run; outputs are read from the state file instead. - Parameters - ---------- - label : str - Human-readable label; also used as the key for dependency - resolution in the :class:`Launcher`. - dirname : str - Directory name to create (relative to CWD). - input_files : list[str | FileFrom] - Files to copy into the project directory before launch. - Items may be plain paths or :class:`FileFrom` objects. - rename_input_files : list[str], optional - If provided (same length as *input_files*), each copied file - is renamed to the corresponding entry. - workflow : Workflow - The inner :class:`Workflow` instance to execute. + Args: + label (str): + Human-readable label; also used as the key for dependency + resolution in the :class:`Launcher`. + dirname (str): + Directory name to create (relative to CWD). + input_files (list[str | FileFrom]): + Files to copy into the project directory before launch. + Items may be plain paths or :class:`FileFrom` objects. + rename_input_files (list[str], optional): + If provided (same length as *input_files*), each copied file + is renamed to the corresponding entry. + workflow (Workflow): + The inner :class:`Workflow` instance to execute. Attributes: - ---------- - output_files : list[str] - Output filenames (populated after launch). - output_values : dict - Scalar results from the inner workflow. - status : str - Current status. - project_dir : str - Absolute path to the project directory. + output_files (list[str]): + Output filenames (populated after launch). + output_values (dict): + Scalar results from the inner workflow. + status (str): + Current status. + project_dir (str): + Absolute path to the project directory. Examples: - -------- - Wrap a VMC optimization in its own directory:: - - enc = Container( - label="vmc-opt", - dirname="01_vmc", - input_files=["hamiltonian_data.h5"], - workflow=VMC_Workflow( - server_machine_name="cluster", - num_opt_steps=10, - target_error=0.001, - ), - ) - status, files, values = enc.launch() + Wrap a VMC optimization in its own directory:: + + enc = Container( + label="vmc-opt", + dirname="01_vmc", + input_files=["hamiltonian_data.h5"], + workflow=VMC_Workflow( + server_machine_name="cluster", + num_opt_steps=10, + target_error=0.001, + ), + ) + status, files, values = enc.launch() See Also: - -------- - Launcher : Execute multiple ``Container`` objects as a DAG. - FileFrom : Reference an output file from another workflow. + Launcher : Execute multiple ``Container`` objects as a DAG. + FileFrom : Reference an output file from another workflow. """ def __init__( @@ -760,9 +739,8 @@ def _copy_input_files(self): """Copy input files into the project directory. Raises: - ------ - FileNotFoundError - If a required input file or directory does not exist. + FileNotFoundError: + If a required input file or directory does not exist. """ for i, src in enumerate(self.input_files): src = str(src) # resolve pathlib objects @@ -818,10 +796,9 @@ def _validate_input_files(self, proj: str): ``WF_Workflow``) *produce* them rather than consume them. Raises: - ------ - FileNotFoundError - With a clear message listing all missing files, raised - *before* any job is submitted. + FileNotFoundError: + With a clear message listing all missing files, raised + *before* any job is submitted. """ missing = [] @@ -974,15 +951,13 @@ async def async_submit(self, action: str = "run") -> dict: :meth:`async_poll` and :meth:`async_collect` to monitor and retrieve results. - Parameters - ---------- - action : str - MCP tool name for action validation. + Args: + action (str): + MCP tool name for action validation. Returns: - ------- - dict - ``{"status": "submitted", "label": ..., "project_dir": ...}``. + dict: + ``{"status": "submitted", "label": ..., "project_dir": ...}``. """ if self._bg_task is not None and not self._bg_task.done(): raise RuntimeError(f"[{self.label}] Already submitted and still running.") @@ -997,9 +972,8 @@ async def async_poll(self) -> dict: """Check whether the container's workflow has completed. Returns: - ------- - dict - Status dict with ``get_workflow_summary()`` when running. + dict: + Status dict with ``get_workflow_summary()`` when running. """ if self._bg_task is None: return {"status": "not_submitted"} @@ -1014,17 +988,15 @@ async def async_collect(self) -> dict: """Collect results from the completed container workflow. Returns: - ------- - dict - ``{"status": ..., "label": ..., "output_files": [...], - **output_values}``. + dict: + ``{"status": ..., "label": ..., "output_files": [...], + **output_values}``. Raises: - ------ - RuntimeError - If not submitted or still running. - Exception - Re-raises the original exception if the workflow failed. + RuntimeError: + If not submitted or still running. + Exception: + Re-raises the original exception if the workflow failed. """ if self._bg_task is None: raise RuntimeError(f"[{self.label}] Not submitted. Call async_submit() first.") diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index d67d733b..f26fe5b8 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -1427,8 +1427,18 @@ def test_analytical_and_auto_grads_and_laplacian_Jastrow_part(j1b_type, j2b_type @pytest.mark.activate_if_skip_heavy @pytest.mark.parametrize("j1b_type,j2b_type,include_nn", _JASTROW_COMBOS) @pytest.mark.parametrize("pattern", ["all_moved", "none_moved", "mixed"]) -def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern: str): - """Compare ratio Jastrow part: debug vs rank-1 update implementation.""" +@pytest.mark.parametrize("n_grid", [1, 3, 6]) +def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern: str, n_grid: int): + """Compare ratio Jastrow part: debug vs rank-1 update implementation. + + Sweeps ``n_grid`` to cover the two production regimes: + * ``n_grid = 1`` -- MCMC walker-update path. Exercises the J2 rewrite + that replaced the (N-1)/N-wasted O(N^2) baseline with on-demand + ``_batch_pairwise_sum`` calls. + * ``n_grid = 3, 6`` -- ECP nonlocal mesh path (Nv = 6 default). + Confirms the rewrite is numerically equivalent when N_grid is large + enough that the old baseline was fully amortized. + """ # Both _compute_ratio_Jastrow_part_rank1_update and _compute_ratio_Jastrow_part_debug # operate in the jastrow_ratio zone (J(R')/J(R) log-ratio). Use that zone's tolerance # to honor the 1-zone-1-module principle. @@ -1436,22 +1446,31 @@ def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern np.random.seed(0) jastrow_data, old_r_up_carts, old_r_dn_carts = _build_jastrow_data_for_part_tests(j1b_type, j2b_type, include_nn) - # Build three grid moves (inlined from the former _build_three_grid_moves_for_jastrow helper) - new_r_up_carts_arr = np.repeat(old_r_up_carts[None, ...], 3, axis=0) - new_r_dn_carts_arr = np.repeat(old_r_dn_carts[None, ...], 3, axis=0) + new_r_up_carts_arr = np.repeat(old_r_up_carts[None, ...], n_grid, axis=0) + new_r_dn_carts_arr = np.repeat(old_r_dn_carts[None, ...], n_grid, axis=0) up_alt_idx = min(1, old_r_up_carts.shape[0] - 1) dn_alt_idx = min(1, old_r_dn_carts.shape[0] - 1) if pattern == "all_moved": - new_r_up_carts_arr[0, 0, 0] += 0.05 - new_r_dn_carts_arr[1, 0, 1] -= 0.07 - new_r_up_carts_arr[2, up_alt_idx, 2] += 0.09 + # Every grid moves exactly one electron; rotate (spin, index, axis) + # mod 3 to cover up_case/dn_case selection and alternate indices. + for g in range(n_grid): + r = g % 3 + if r == 0: + new_r_up_carts_arr[g, 0, 0] += 0.05 + elif r == 1: + new_r_dn_carts_arr[g, 0, 1] -= 0.07 + else: + new_r_up_carts_arr[g, up_alt_idx, 2] += 0.09 elif pattern == "none_moved": pass elif pattern == "mixed": + # Move on first grid; if n_grid >= 3 also move on the last grid (the + # interior grids stay put -- their ratio must be exactly 1.0). new_r_up_carts_arr[0, 0, 0] += 0.05 - new_r_dn_carts_arr[2, dn_alt_idx, 1] += 0.04 + if n_grid >= 3: + new_r_dn_carts_arr[n_grid - 1, dn_alt_idx, 1] += 0.04 ratio_debug = _compute_ratio_Jastrow_part_debug( jastrow_data, @@ -1469,6 +1488,7 @@ def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern new_r_dn_carts_arr=new_r_dn_carts_arr, ) + assert ratio_auto.shape == (n_grid,) assert not np.any(np.isnan(np.asarray(np.asarray(ratio_debug)))), "NaN detected in first argument" assert not np.any(np.isnan(np.asarray(np.asarray(ratio_auto)))), "NaN detected in second argument" np.testing.assert_allclose(np.asarray(ratio_debug), np.asarray(ratio_auto), atol=atol, rtol=rtol) From 282a29e4c1575cb3f597ef60e5fd2c2e656bca82 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sat, 16 May 2026 12:06:07 +0900 Subject: [PATCH 67/97] Optimize J2/J3 in MCMC update and clean up jqmc_workflow (#74) * Update docs. * Optimize J2 ratio: O(N^2) baseline -> O(N * N_grid) per-grid sums * Introduce J3 state carry in MCMC update. * Implement the slim J3 carry for MCMC WF update. * Refactor atomic_orbital: coalesce primitive gather and lift r-R to unique-atom rank Address the L1 LSU wavefront-pipe bottleneck (~90% of peak) identified in `_compute_AOs_cart` `_compute_AOs_sphe` and their Laplacians. Coalesced bucket reduction: - Introduce `PrimBucketLayout` (NamedTuple) and rewrite `_build_prim_buckets_by_K` to return a permanent bucket-K-major permutation of the primitive axis. - Apply the permutation inside every `_*_prim_*` accessor on `AOs_cart_data` / `AOs_sphe_data`, so all primitive arrays are stored bucket-K-major. - `_reduce_primitives_to_aos` now reduces each K bucket via `dynamic_slice + reshape + reduce_sum` instead of a fancy primitive-index gather, collapsing L1 sectors-per-request from ~10 toward the ideal of 4. r-R at unique-atom rank: - Compute `r-R` and `r^2` at unique-atom rank (`n_atoms_unique << num_ao_prim`) and gather to primitive rank via `_nucleus_index_prim_jnp`. - Eliminates the `(num_ao_prim, n_elec, 3)` broadcast intermediate that drove most of the register-spill local-memory traffic. - Applied to `_compute_AOs_cart`, `_compute_AOs_sphe`, `_compute_AOs_laplacian_analytic_cart`, and `_compute_AOs_laplacian_analytic_sphe` (the spherical Laplacian additionally computes `grad_S . r_R` at unique rank). * Fixed a trivial bug atomic_orbital: permute primitive arrays along the last axis `_exponents_jnp` / `_coefficients_jnp` applied the bucket-K-major permutation via `arr[self._prim_perm_np]`, which targets axis 0. This is fine for the AO eval kernels (1D primitive arrays), but `collect_param_grads` reads these accessors on a vmap-batched `AOs_*_data` whose `exponents` / `coefficients` leaves are 2D `(num_walkers, num_ao_prim)`, the take then mangled the walker axis into the primitive axis and produced shape mismatches downstream. Switch to `jnp.take(arr, self._prim_perm_np, axis=-1)` so the permutation is always applied to the primitive axis regardless of prepended batch dims. AO-eval results are bit-exact identical for the 1D inputs used by `_compute_AOs_*`. * Fixed a bug in AOs. return raw-order exponents/coefficients from public accessors The public ao_exponents/ao_coefficients on Geminal_data and Jastrow_three_body_data were leaking the bucket-K-major permuted form (_exponents_jnp/_coefficients_jnp), which broke round-trips through the basis-optimization API (ShellPrimMap.symmetrize, with_updated_ao_*, apply_block_update) and produced double-mangled values. Public accessors now return basis-natural order via jnp.asarray(self.exponents/coefficients). The bucket-K permutation stays strictly inside the AO eval kernel boundary. Also revert the jnp.take(..., axis=-1) workaround introduced in 561ca40 in atomic_orbital._{exponents,coefficients}_jnp. * Revert "Fixed a bug in AOs. return raw-order exponents/coefficients from public accessors" This reverts commit f38dd2193183f0d77806433c0a6bfdf53714ba72. * Revert "Fixed a trivial bug atomic_orbital: permute primitive arrays along the last axis" This reverts commit 561ca4087ddd77b02deee9d13d467be543aa013f. * Revert "Refactor atomic_orbital: coalesce primitive gather and lift r-R to unique-atom rank" This reverts commit 43bfa00287b3ffe80711b8128ab9beeba511419d. --- jqmc/jastrow_factor.py | 182 +++++++++++++++++++++++++++++++++++++++++ jqmc/jqmc_mcmc.py | 113 +++++++++++++++++++++++-- tests/test_jastrow.py | 66 ++++++++++++++- 3 files changed, 352 insertions(+), 9 deletions(-) diff --git a/jqmc/jastrow_factor.py b/jqmc/jastrow_factor.py index 79b333e3..b687530a 100644 --- a/jqmc/jastrow_factor.py +++ b/jqmc/jastrow_factor.py @@ -4353,6 +4353,188 @@ def _branch_dn(_): return jax.lax.cond(moved_spin_is_up, _branch_up, _branch_dn, operand=None) +# ----------------------------------------------------------------------------- +# Slim ratio-only J3 streaming state (8 fields, no grad/lap). +# +# Mirrors the ``Det_ratio_streaming_state`` / ``Det_streaming_state`` split: +# ``_compute_ratio_Jastrow_part_rank1_update`` only reads the 8 fields below +# (aos_*, j3_mat_aos_*, j3_mat_T_aos_*, rowsums), so the MCMC walker update +# path -- which never consumes grad/lap -- can carry just these and skip the +# remaining 10 fields the full state holds for LRDMC kinetic-energy work. +# +# Cuts ~63% of the per-walker state bytes (grad_aos_* alone is 3x larger via +# the trailing xyz axis), removing the bulk of the DtoD plumbing traffic the +# full state would cost in an MCMC ``fori_loop`` carry. +# ----------------------------------------------------------------------------- + + +@struct.dataclass +class Jastrow_three_body_ratio_state: + """Slim ratio-only J3 streaming state. + + Subset of :class:`Jastrow_three_body_streaming_state` containing exactly + the 8 fields read by :func:`_compute_ratio_Jastrow_part_rank1_update` when + a J3 cache is supplied. Field names match the full state so the ratio + kernel consumes either via duck-typing (mirrors the + ``Det_ratio_streaming_state`` / ``Det_streaming_state`` pattern). + + Fields (shapes use ``n_orb`` for the orbital dimension; for MO-based + three-body the same ``n_orb`` is used, since orbitals are evaluated by + ``compute_MOs`` to dimension ``orb_data._num_orb``): + + - ``aos_up`` / ``aos_dn``: ``(n_orb, N_up)`` / ``(n_orb, N_dn)`` orbital values. + - ``j3_mat_aos_up`` / ``j3_mat_aos_dn``: ``j3_mat @ aos_*`` (shapes match aos_*). + - ``j3_mat_T_aos_up`` / ``j3_mat_T_aos_dn``: ``j3_mat.T @ aos_*``. + - ``j3_mat_aos_dn_rowsum`` / ``j3_mat_T_aos_up_rowsum``: ``(n_orb,)`` -- the + ``dn_cross_vec`` / ``up_cross_vec`` consumed by the ratio kernel. + """ + + aos_up: jax.Array = struct.field(pytree_node=True) + aos_dn: jax.Array = struct.field(pytree_node=True) + j3_mat_aos_up: jax.Array = struct.field(pytree_node=True) + j3_mat_aos_dn: jax.Array = struct.field(pytree_node=True) + j3_mat_T_aos_up: jax.Array = struct.field(pytree_node=True) + j3_mat_T_aos_dn: jax.Array = struct.field(pytree_node=True) + j3_mat_aos_dn_rowsum: jax.Array = struct.field(pytree_node=True) + j3_mat_T_aos_up_rowsum: jax.Array = struct.field(pytree_node=True) + + +@jit +def _init_jastrow_ratio_state( + jastrow_three_body_data: Jastrow_three_body_data, + r_up_carts: jax.Array, + r_dn_carts: jax.Array, +) -> Jastrow_three_body_ratio_state: + """Initialize the slim ratio-only J3 streaming state at ``(r_up, r_dn)``. + + Value-only AO evaluation (no grad/lap), four ``(n_orb, n_orb) @ (n_orb, N_e)`` + matmuls, and two ``(n_orb,)`` row-sums. Subsequent advances refresh only + one column per accepted single-electron move. + + Cost: dominated by 2 ``compute_orb`` calls + 4 matmuls, equivalent to the + j3_state-less branch of :func:`_compute_ratio_Jastrow_part_rank1_update`. + """ + # Use the ratio-zone dtype to match what + # ``_compute_ratio_Jastrow_part_rank1_update`` will cast its inputs to; + # this avoids a re-cast on first consumption. + dtype_jnp = get_dtype_jnp("jastrow_ratio") + orb_data = jastrow_three_body_data.orb_data + compute_orb, _compute_orb_grad, _compute_orb_lapl, _compute_orb_vgl = _three_body_orb_apis(jastrow_three_body_data) + + # Value-only AO eval. Forward r_*_carts unchanged so the underlying + # kernels reconstruct r-R in fp64 (Principle 3b). + aos_up = jnp.asarray(compute_orb(orb_data, r_up_carts), dtype=dtype_jnp) + aos_dn = jnp.asarray(compute_orb(orb_data, r_dn_carts), dtype=dtype_jnp) + + j_matrix = jastrow_three_body_data._j_matrix_jnp.astype(dtype_jnp) + j3_mat = j_matrix[:, :-1] + + j3_mat_aos_up = j3_mat @ aos_up + j3_mat_T_aos_up = j3_mat.T @ aos_up + j3_mat_aos_dn = j3_mat @ aos_dn + j3_mat_T_aos_dn = j3_mat.T @ aos_dn + # Row-sums consumed as ``dn_cross_vec`` / ``up_cross_vec`` by the ratio kernel. + j3_mat_aos_dn_rowsum = jnp.sum(j3_mat_aos_dn, axis=1) + j3_mat_T_aos_up_rowsum = jnp.sum(j3_mat_T_aos_up, axis=1) + + return Jastrow_three_body_ratio_state( + aos_up=aos_up, + aos_dn=aos_dn, + j3_mat_aos_up=j3_mat_aos_up, + j3_mat_aos_dn=j3_mat_aos_dn, + j3_mat_T_aos_up=j3_mat_T_aos_up, + j3_mat_T_aos_dn=j3_mat_T_aos_dn, + j3_mat_aos_dn_rowsum=j3_mat_aos_dn_rowsum, + j3_mat_T_aos_up_rowsum=j3_mat_T_aos_up_rowsum, + ) + + +@jit +def _advance_jastrow_ratio_state( + jastrow_three_body_data: Jastrow_three_body_data, + state: Jastrow_three_body_ratio_state, + moved_spin_is_up: jax.Array, + moved_index: jax.Array, + r_up_carts_new: jax.Array, + r_dn_carts_new: jax.Array, +) -> Jastrow_three_body_ratio_state: + """Advance the slim ratio-only J3 streaming state after a single-electron move. + + Mirrors :func:`_advance_grads_laplacian_Jastrow_three_body_streaming_state` + but with the grad/lap field plumbing removed: + + - Single-point AO eval at the moved electron's new position (value only, + no grad/lap so the per-call ``compute_orb_vgl`` becomes ``compute_orb``). + - ``delta_aos = aos_new - state.aos[:, moved_index]``, + ``d_J = j3_mat @ delta_aos``, ``d_JT = j3_mat.T @ delta_aos``. + - Refresh the moved column of ``j3_mat_aos_*`` / ``j3_mat_T_aos_*`` via + ``.at[:, moved_index].add(...)`` and the corresponding row-sum. + + Cost: ``O(n_ao + n_ao^2)`` per call -- one single-electron AO eval plus + two ``n_ao``-sized matvecs. Strictly less work than the full advance: + the grad/lap einsums and the ``g_up`` / ``g_dn`` triangular updates are + not needed for the ratio kernel. + """ + dtype_jnp = get_dtype_jnp("jastrow_ratio") + orb_data = jastrow_three_body_data.orb_data + compute_orb, _compute_orb_grad, _compute_orb_lapl, _compute_orb_vgl = _three_body_orb_apis(jastrow_three_body_data) + + j_matrix = jastrow_three_body_data._j_matrix_jnp.astype(dtype_jnp) + j3_mat = j_matrix[:, :-1] + + num_up = state.aos_up.shape[1] + num_dn = state.aos_dn.shape[1] + + def _branch_up(_): + # Single-electron AO eval at the new up position (value only). + r_new = jnp.expand_dims(r_up_carts_new[moved_index], axis=0) # (1, 3) + aos_new_col = jnp.asarray(compute_orb(orb_data, r_new)[:, 0], dtype=dtype_jnp) + + delta_aos = aos_new_col - state.aos_up[:, moved_index] + d_J = j3_mat @ delta_aos # (n_orb,) + d_JT = j3_mat.T @ delta_aos # (n_orb,) + + new_aos_up = state.aos_up.at[:, moved_index].set(aos_new_col) + new_j3_mat_aos_up = state.j3_mat_aos_up.at[:, moved_index].add(d_J) + new_j3_mat_T_aos_up = state.j3_mat_T_aos_up.at[:, moved_index].add(d_JT) + # Only the moved up column of ``j3_mat_T_aos_up`` changes -> rowsum + # picks up exactly ``d_JT``. ``j3_mat_aos_dn_rowsum`` is unchanged + # (depends on aos_dn only). + return state.replace( + aos_up=new_aos_up, + j3_mat_aos_up=new_j3_mat_aos_up, + j3_mat_T_aos_up=new_j3_mat_T_aos_up, + j3_mat_T_aos_up_rowsum=state.j3_mat_T_aos_up_rowsum + d_JT, + ) + + def _branch_dn(_): + r_new = jnp.expand_dims(r_dn_carts_new[moved_index], axis=0) + aos_new_col = jnp.asarray(compute_orb(orb_data, r_new)[:, 0], dtype=dtype_jnp) + + delta_aos = aos_new_col - state.aos_dn[:, moved_index] + d_J = j3_mat @ delta_aos + d_JT = j3_mat.T @ delta_aos + + new_aos_dn = state.aos_dn.at[:, moved_index].set(aos_new_col) + new_j3_mat_aos_dn = state.j3_mat_aos_dn.at[:, moved_index].add(d_J) + new_j3_mat_T_aos_dn = state.j3_mat_T_aos_dn.at[:, moved_index].add(d_JT) + # Mirror branch_up: only the moved dn column of ``j3_mat_aos_dn`` + # changes -> rowsum picks up ``d_J``. Up rowsum is unchanged. + return state.replace( + aos_dn=new_aos_dn, + j3_mat_aos_dn=new_j3_mat_aos_dn, + j3_mat_T_aos_dn=new_j3_mat_T_aos_dn, + j3_mat_aos_dn_rowsum=state.j3_mat_aos_dn_rowsum + d_J, + ) + + # Edge case: zero-electron spin sector -- the cond collapses at trace time. + if num_up == 0: + return _branch_dn(None) + if num_dn == 0: + return _branch_up(None) + return jax.lax.cond(moved_spin_is_up, _branch_up, _branch_dn, operand=None) + + def _compute_grads_and_laplacian_Jastrow_three_body_debug( jastrow_three_body_data: Jastrow_three_body_data, r_up_carts: np.ndarray, diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index 5e8ad004..6d67ad02 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -87,7 +87,12 @@ compute_local_energy, compute_local_energy_fast, ) -from .jastrow_factor import _compute_ratio_Jastrow_part_rank1_update, compute_Jastrow_part +from .jastrow_factor import ( + _advance_jastrow_ratio_state, + _compute_ratio_Jastrow_part_rank1_update, + _init_jastrow_ratio_state, + compute_Jastrow_part, +) from .structure import _find_nearest_index_jnp from .swct import evaluate_swct_domega, evaluate_swct_omega from .wavefunction import evaluate_ln_wavefunction, evaluate_ln_wavefunction_fast @@ -734,6 +739,18 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__latest_r_up_carts, self.__latest_r_dn_carts, ) + # Same throwaway-for-trace pattern for the J3 streaming state; ``None`` + # when no three-body Jastrow component is present (Python-static + # dispatch keeps the trace consistent with the chain-entry value below). + _j3d_warmup = self.__hamiltonian_data.wavefunction_data.jastrow_data.jastrow_three_body_data + if _j3d_warmup is not None: + j3_state_warmup = _jit_vmap_init_j3_state( + _j3d_warmup, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + ) + else: + j3_state_warmup = None dtype_jnp = jnp.float64 dtype_np = np.float64 @@ -758,6 +775,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: geminal_inv, geminal, det_ratio_state_warmup, + j3_state_warmup, ) else: _ = _jit_vmap_update( @@ -771,6 +789,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: geminal_inv, geminal, det_ratio_state_warmup, + j3_state_warmup, ) _ = _jit_vmap_e_L_fast( self.__hamiltonian_data, @@ -905,6 +924,22 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: self.__latest_r_dn_carts, ) + # J3 streaming state: skip the O(n_ao^2 * N_e) matmul rebuild of + # ``W_up/W_dn = j3_mat @ aos_*`` and ``U_up/U_dn = j3_mat.T @ aos_*`` + # on every MCMC ratio call by maintaining the rank-1-updatable cache + # alongside ``det_ratio_state``. Only constructed when a three-body + # Jastrow component is present; otherwise pass-through ``None`` keeps + # the rank-1 ratio path on its existing (no-cache) trace. + _j3d = self.__hamiltonian_data.wavefunction_data.jastrow_data.jastrow_three_body_data + if _j3d is not None: + j3_state = _jit_vmap_init_j3_state( + _j3d, + self.__latest_r_up_carts, + self.__latest_r_dn_carts, + ) + else: + j3_state = None + mcmc_loop_start = time.perf_counter() for i_mcmc_step in range(num_mcmc_steps): if i_mcmc_step % mcmc_interval == 0: @@ -926,6 +961,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: geminal_inv, geminal, det_ratio_state, + j3_state, ) = _jit_vmap_update_up( self.__latest_r_up_carts, self.__latest_r_dn_carts, @@ -937,6 +973,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: geminal_inv, geminal, det_ratio_state, + j3_state, ) else: ( @@ -948,6 +985,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: geminal_inv, geminal, det_ratio_state, + j3_state, ) = _jit_vmap_update( self.__latest_r_up_carts, self.__latest_r_dn_carts, @@ -959,6 +997,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: geminal_inv, geminal, det_ratio_state, + j3_state, ) self.__latest_r_up_carts.block_until_ready() self.__latest_r_dn_carts.block_until_ready() @@ -4800,6 +4839,7 @@ def _update_electron_positions( geminal_inv_init, geminal_init, det_ratio_state_init, + j3_state_init, ): """Update electron positions based on the MH method. @@ -4818,6 +4858,17 @@ def _update_electron_positions( ``(init_r_up_carts, init_r_dn_carts)``. Built once at the MCMC chain entry by :func:`_init_det_ratio_streaming_state` and advanced via :func:`_advance_det_ratio_streaming_state` on accept. + j3_state_init: Optional ``Jastrow_three_body_ratio_state`` consistent + with ``(init_r_up_carts, init_r_dn_carts)``. When ``jastrow_three_body_data`` + is present, built once at the MCMC chain entry and advanced via + :func:`_advance_jastrow_ratio_state` on accept; passing it to + ``_compute_ratio_Jastrow_part_rank1_update`` avoids the per-step + ``O(n_ao^2 * N_e)`` matmul rebuild. ``None`` when no three-body + Jastrow component exists. The slim ratio-state mirrors the + ``Det_ratio_streaming_state`` pattern: only the 8 fields the ratio + kernel reads (aos_*, j3_mat_aos_*, j3_mat_T_aos_*, rowsums) are + carried through the ``fori_loop``, avoiding the grad/lap plumbing + cost the full LRDMC-kinetic state would impose. Returns: jax_PRNG_key (jnpt.ArrayLike): updated jax_PRNG_key. @@ -4834,6 +4885,7 @@ def _update_electron_positions( geminal = geminal_init geminal_inv = geminal_inv_init det_ratio_state = det_ratio_state_init + j3_state = j3_state_init def body_fun(_, carry): ( @@ -4845,6 +4897,7 @@ def body_fun(_, carry): geminal_inv, geminal, det_ratio_state, + j3_state, ) = carry total_electrons = len(r_up_carts) + len(r_dn_carts) num_up_electrons = len(r_up_carts) @@ -4931,13 +4984,17 @@ def body_fun(_, carry): * (1.0 / (2.0 * f_prime_l**2 * Dt**2) - 1.0 / (2.0 * f_l**2 * Dt**2)) ) - # Jastrow ratio via dedicated fast-update API (includes exp) + # Jastrow ratio via dedicated fast-update API (includes exp). + # ``j3_state`` (when not None) lets the J3 block reuse cached + # ``aos_*`` and ``j3_mat @ aos_*`` / ``j3_mat.T @ aos_*`` precontracts + # instead of recomputing the O(n_ao^2 * N_e) matmuls each step. J_ratio = _compute_ratio_Jastrow_part_rank1_update( jastrow_data=hamiltonian_data.wavefunction_data.jastrow_data, old_r_up_carts=r_up_carts, old_r_dn_carts=r_dn_carts, new_r_up_carts_arr=jnp.expand_dims(proposed_r_up_carts, axis=0), new_r_dn_carts_arr=jnp.expand_dims(proposed_r_dn_carts, axis=0), + j3_state=j3_state, )[0] # Determinant part, fast update using the matrix determinant lemma. @@ -5032,6 +5089,22 @@ def body_fun(_, carry): r_dn_carts_new=proposed_r_dn_carts, ) + # On accept, advance the J3 streaming state to the new configuration. + # Python-static dispatch on ``jastrow_three_body_data is None`` -- when + # no J3 component exists ``j3_state`` is ``None`` and we just pass it + # through (also avoids tracing the advance kernel). + if hamiltonian_data.wavefunction_data.jastrow_data.jastrow_three_body_data is not None: + j3_state_new = _advance_jastrow_ratio_state( + jastrow_three_body_data=hamiltonian_data.wavefunction_data.jastrow_data.jastrow_three_body_data, + state=j3_state, + moved_spin_is_up=is_up, + moved_index=selected_electron_index, + r_up_carts_new=proposed_r_up_carts, + r_dn_carts_new=proposed_r_dn_carts, + ) + else: + j3_state_new = j3_state + def _accepted_fun(_): # Move accepted return ( @@ -5042,6 +5115,7 @@ def _accepted_fun(_): geminal_inv_new, geminal_new, det_ratio_state_new, + j3_state_new, ) def _rejected_fun(_): @@ -5054,6 +5128,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ) # judge accept or reject the propsed move using jax.lax.cond @@ -5065,6 +5140,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ) = lax.cond(b < acceptance_ratio, _accepted_fun, _rejected_fun, operand=None) carry = ( @@ -5076,6 +5152,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ) return carry @@ -5089,6 +5166,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ) = jax.lax.fori_loop( 0, num_mcmc_per_measurement, @@ -5102,6 +5180,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ), ) @@ -5114,6 +5193,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ) @@ -5129,12 +5209,20 @@ def _update_electron_positions_only_up_electron( geminal_inv_init, geminal_init, det_ratio_state_init, + j3_state_init, ): """Update electron positions based on the MH method (up-spin electrons only). See :func:`_update_electron_positions` for the slim state ``det_ratio_state_init`` contract; here only up-electrons move so ``state.ao_dn`` and ``state.paired_dn`` stay constant for the entire chain. + + ``j3_state_init`` mirrors the signature of :func:`_update_electron_positions` + so the vmap wrappers stay symmetric, but this variant evaluates the Jastrow + factor via the legacy ``compute_Jastrow_part`` path rather than the + rank-1 ratio API, so the state is only carried through (never consumed + or advanced). A future refactor to the ratio path would also light up + the same J3 streaming-cache savings here. """ dtype_jnp = jnp.float64 accepted_moves = 0 @@ -5144,6 +5232,7 @@ def _update_electron_positions_only_up_electron( geminal_inv = geminal_inv_init geminal = geminal_init det_ratio_state = det_ratio_state_init + j3_state = j3_state_init def body_fun(_, carry): ( @@ -5155,6 +5244,7 @@ def body_fun(_, carry): geminal_inv, geminal, det_ratio_state, + j3_state, ) = carry num_up_electrons = len(r_up_carts) @@ -5296,7 +5386,10 @@ def body_fun(_, carry): ) def _accepted_fun(_): - # Move accepted + # Move accepted. ``j3_state`` is carried through unchanged: this + # variant uses the legacy full-Jastrow path (``compute_Jastrow_part``) + # rather than the rank-1 ratio API, so the J3 streaming cache is + # not consumed or advanced here. return ( accepted_moves + 1, rejected_moves, @@ -5305,6 +5398,7 @@ def _accepted_fun(_): geminal_inv_new, geminal_new, det_ratio_state_new, + j3_state, ) def _rejected_fun(_): @@ -5317,6 +5411,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ) # judge accept or reject the propsed move using jax.lax.cond @@ -5328,6 +5423,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ) = lax.cond(b < acceptance_ratio, _accepted_fun, _rejected_fun, operand=None) carry = ( @@ -5339,6 +5435,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ) return carry @@ -5352,6 +5449,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ) = jax.lax.fori_loop( 0, num_mcmc_per_measurement, @@ -5365,6 +5463,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ), ) @@ -5377,6 +5476,7 @@ def _rejected_fun(_): geminal_inv, geminal, det_ratio_state, + j3_state, ) @@ -5384,16 +5484,19 @@ def _rejected_fun(_): # Created once at import time so subsequent MCMC.run() calls reuse # the same Python function objects and hit JAX's compilation cache. _jit_vmap_update = jit( - vmap(_update_electron_positions, in_axes=(0, 0, 0, None, None, None, None, 0, 0, 0)), + vmap(_update_electron_positions, in_axes=(0, 0, 0, None, None, None, None, 0, 0, 0, 0)), static_argnums=3, ) _jit_vmap_update_up = jit( - vmap(_update_electron_positions_only_up_electron, in_axes=(0, 0, 0, None, None, None, None, 0, 0, 0)), + vmap(_update_electron_positions_only_up_electron, in_axes=(0, 0, 0, None, None, None, None, 0, 0, 0, 0)), static_argnums=3, ) _jit_vmap_init_det_ratio_state = jit( vmap(_init_det_ratio_streaming_state, in_axes=(None, 0, 0)), ) +_jit_vmap_init_j3_state = jit( + vmap(_init_jastrow_ratio_state, in_axes=(None, 0, 0)), +) _jit_vmap_e_L_fast = jit(vmap(compute_local_energy_fast, in_axes=(None, 0, 0, 0, 0, 0))) _jit_vmap_as_reg = jit(vmap(compute_AS_regularization_factor, in_axes=(None, 0, 0))) _jit_vmap_generate_RTs = jit(vmap(_generate_rotation_matrix, in_axes=0)) diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index f26fe5b8..0d6b3245 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -64,6 +64,7 @@ _compute_Jastrow_two_body_debug, _compute_ratio_Jastrow_part_debug, _compute_ratio_Jastrow_part_rank1_update, + _init_grads_laplacian_Jastrow_three_body_streaming_state, compute_grads_and_laplacian_Jastrow_one_body, compute_grads_and_laplacian_Jastrow_part, compute_grads_and_laplacian_Jastrow_three_body, @@ -1424,11 +1425,50 @@ def test_analytical_and_auto_grads_and_laplacian_Jastrow_part(j1b_type, j2b_type jax.clear_caches() +# Hand-picked covering set replacing the 5x3x3x2 = 90-case full Cartesian +# product. The default Jastrow combo gets a full (pattern, n_grid, +# use_j3_state) sweep because the rank-1 update + J3 streaming-cache logic +# lives outside the J1/J2/NN function paths -- so the other four Jastrow +# combos only need representative configs to confirm those paths still +# integrate. ``mixed`` + ``n_grid=1`` is omitted because the test body's +# ``if n_grid >= 3`` branch does not fire there, making it a duplicate of +# ``all_moved`` + ``n_grid=1``. All single-value coverage and the +# (n_grid x use_j3_state), (pattern x use_j3_state), and valid +# (pattern x n_grid) pairs are preserved. +_RATIO_RANK1_PARAM_SETS = [ + # (j1b_type, j2b_type, include_nn, pattern, n_grid, use_j3_state) + # --- default Jastrow combo: full (pattern, n_grid, use_j3_state) sweep --- + ("exp", "pade", False, "all_moved", 1, False), + ("exp", "pade", False, "all_moved", 1, True), + ("exp", "pade", False, "all_moved", 3, False), + ("exp", "pade", False, "all_moved", 3, True), + ("exp", "pade", False, "all_moved", 6, False), + ("exp", "pade", False, "all_moved", 6, True), + ("exp", "pade", False, "none_moved", 1, False), + ("exp", "pade", False, "none_moved", 6, True), + ("exp", "pade", False, "mixed", 3, False), + ("exp", "pade", False, "mixed", 3, True), + ("exp", "pade", False, "mixed", 6, False), + ("exp", "pade", False, "mixed", 6, True), + # --- other Jastrow combos: representative configs --- + ("pade", "pade", False, "all_moved", 3, True), + ("pade", "pade", False, "none_moved", 1, False), + ("exp", "exp", False, "all_moved", 6, False), + ("exp", "exp", False, "mixed", 6, True), + ("pade", "exp", False, "all_moved", 1, True), + ("pade", "exp", False, "mixed", 3, False), + ("exp", "pade", True, "all_moved", 1, False), + ("exp", "pade", True, "none_moved", 3, True), + ("exp", "pade", True, "mixed", 6, True), +] + + @pytest.mark.activate_if_skip_heavy -@pytest.mark.parametrize("j1b_type,j2b_type,include_nn", _JASTROW_COMBOS) -@pytest.mark.parametrize("pattern", ["all_moved", "none_moved", "mixed"]) -@pytest.mark.parametrize("n_grid", [1, 3, 6]) -def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern: str, n_grid: int): +@pytest.mark.parametrize( + "j1b_type,j2b_type,include_nn,pattern,n_grid,use_j3_state", + _RATIO_RANK1_PARAM_SETS, +) +def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern: str, n_grid: int, use_j3_state: bool): """Compare ratio Jastrow part: debug vs rank-1 update implementation. Sweeps ``n_grid`` to cover the two production regimes: @@ -1438,6 +1478,12 @@ def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern * ``n_grid = 3, 6`` -- ECP nonlocal mesh path (Nv = 6 default). Confirms the rewrite is numerically equivalent when N_grid is large enough that the old baseline was fully amortized. + + ``use_j3_state`` toggles the J3 streaming-cache path inside the rank-1 + update (``j3_state=None`` -> recompute aos / j3_mat @ aos every call; + ``j3_state=init(...)`` -> read from cache). Both must agree with the + debug reference; this is the regression guard for the MCMC walker + update integration of the cache. """ # Both _compute_ratio_Jastrow_part_rank1_update and _compute_ratio_Jastrow_part_debug # operate in the jastrow_ratio zone (J(R')/J(R) log-ratio). Use that zone's tolerance @@ -1480,12 +1526,24 @@ def test_ratio_Jastrow_part_rank1_update(j1b_type, j2b_type, include_nn, pattern new_r_dn_carts_arr=new_r_dn_carts_arr, ) + # Build the J3 streaming cache from the OLD configuration when requested. + # ``_build_jastrow_data_for_part_tests`` always includes a J3 component, + # so the cache is always constructible here. + j3_state = None + if use_j3_state: + j3_state = _init_grads_laplacian_Jastrow_three_body_streaming_state( + jastrow_data.jastrow_three_body_data, + old_r_up_carts, + old_r_dn_carts, + ) + ratio_auto = _compute_ratio_Jastrow_part_rank1_update( jastrow_data, old_r_up_carts=old_r_up_carts, old_r_dn_carts=old_r_dn_carts, new_r_up_carts_arr=new_r_up_carts_arr, new_r_dn_carts_arr=new_r_dn_carts_arr, + j3_state=j3_state, ) assert ratio_auto.shape == (n_grid,) From 601ba2d78798c46a16643e6cdfa24e1a1523a5b2 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Mon, 18 May 2026 14:22:07 +0900 Subject: [PATCH 68/97] Update changelog.md --- doc/changelog.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/doc/changelog.md b/doc/changelog.md index ac69f25a..3c2fbd57 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,39 @@ # Change Log +## May-18-2026: v0.2.2a1 + +This release brings configurable mixed-precision support, deep kernel-level performance work (AOs, Jastrow, det/Jastrow ratios, GFMC), on-GPU VMC optimization, and a project-wide lint/cleanup. + +### New features + +* **Mixed precision support**: Added a configurable precision system with per-zone dtype control (fp64 by default; fp32 in selected zones in `"mixed"` mode). Refactored as selectable-precision modules with three explicit design principles. The geminal/AGP/SD path remains in fp64 to prevent fp32 amplification of `log|det|`, and electron-nucleus `r - R` differences are computed in fp64 before downcasting to avoid catastrophic cancellation. `ao_grad_lap` and `mo_grad_lap` precision zones are split for finer-grained control. The public API is reduced to a single mode selector: `"full"` or `"mixed"`. +* **On-GPU VMC optimization**: VMC parameter optimization can now run entirely on GPU. Added `use_device_collectives` (auto-selected by the JAX backend: GPU=`True`, otherwise `False`) with an MPI/JAX consistency check, along with matching CLI/TOML options. Multi-GPU `run_optimize` is supported. +* **Ruff lint pipeline**: Added `jqmc-lint-ruff.yml` GitHub Action and updated `.pre-commit-config.yaml`. Applied auto-fixes and manual cleanups across the codebase, and removed non-ASCII characters from code and docstrings. + +### Performance & memory + +* **AO module (HLO-level)**: Reduced L1/L2/DRAM traffics. Replaced `segment_sum` with a bucketed reduce+gather scheme (including `V_l`). Unrolled `(8Z)**l` in the Cartesian kernels to avoid XLA `while` loops. Removed `eps` from Cartesian GTOs. Fused AO/MO value/grad/lap into a single dispatch on hot paths. +* **Streaming caches on hot paths**: Stream cached AO and paired tables into every det-ratio / jas-ratio hot paths. Improved the J3 streaming cache in `jastrow_factor.py`. Reused J3 streaming-state AOs in LRDMC mesh / ECP non-local ratios. Improved the K state carry in `jqmc/wavefunction.py`. +* **Jastrow ratios**: Optimized J2 ratio from an `O(N^2)` baseline to `O(N * N_grid)` per-grid sums. Introduced a slim J3 state carry in the MCMC wavefunction update. Polished Jastrow with a dense `(N, N)` up-up / dn-dn pair reduction and removed scatter-add `while` loops. +* **GFMC_t**: Added a streaming kinetic-state path (parity with GFMC_n) and replaced a Python `while` loop with `lax.while_loop` in the projection. +* **Misc**: Vectorized the electron-configuration generator and PRNG key initialization. Switched the jackknife standard deviation to a two-pass centered sum-of-squares for better numerical stability. Replaced `hessian()` with `jvp(grad)` for the NN-Jastrow Laplacian. + +### Bug fixes + +* **GFMC/MCMC logging**: Improved loggers in `jqmc/jqmc_gfmc.py` and `jqmc/jqmc_mcmc.py`. + +### Workflow (`jqmc_workflow`) + +* Refactored workflow modules (`vmc_workflow.py`, `mcmc_workflow.py`, `lrdmc_workflow.py`, `lrdmc_ext_workflow.py`, `workflow.py`, and the `_*.py` helpers) for readability, with no behavior change. + +### Tests & infrastructure + +* Polished tolerance control across the test suite; introduced a medium tolerance for numerical-Laplacian tests and removed the separate autodiff tolerance. +* Removed `test_kinetic_energy_analytic_and_numerical` and `test_numerial_and_auto_grads_and_laplacians_ln_Det` because the numerical references are intrinsically unstable; analytical versions are already validated against JAX autograd. +* Shortened `jqmc-run-full-pytest.yml` and updated GitHub Actions. +* Made the numerical-Laplacian debug functions more stable. + ## Apr-24-2026: v0.2.1a2 Minor update focusing on workflow improvements, bug fixes, and new benchmark infrastructure. From 40342b2b0d815093a2a829f62c883c40def41408 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Mon, 18 May 2026 22:39:01 +0900 Subject: [PATCH 69/97] Split rc test. --- ... => jqmc-run-rc-full-precision-pytest.yml} | 12 ---- .../jqmc-run-rc-mixed-precision-pytest.yml | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 12 deletions(-) rename .github/workflows/{jqmc-run-rc-pytest.yml => jqmc-run-rc-full-precision-pytest.yml} (75%) create mode 100644 .github/workflows/jqmc-run-rc-mixed-precision-pytest.yml diff --git a/.github/workflows/jqmc-run-rc-pytest.yml b/.github/workflows/jqmc-run-rc-full-precision-pytest.yml similarity index 75% rename from .github/workflows/jqmc-run-rc-pytest.yml rename to .github/workflows/jqmc-run-rc-full-precision-pytest.yml index 97165f1c..07e43316 100644 --- a/.github/workflows/jqmc-run-rc-pytest.yml +++ b/.github/workflows/jqmc-run-rc-full-precision-pytest.yml @@ -68,18 +68,6 @@ jobs: mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py - - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) - run: | - pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed - pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed - pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - - - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) - run: | - mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed - mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed - mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - - name: Test jqmc-tool (toolset for jqmc) run: | pytest -s -v tests/test_jqmc_tool.py diff --git a/.github/workflows/jqmc-run-rc-mixed-precision-pytest.yml b/.github/workflows/jqmc-run-rc-mixed-precision-pytest.yml new file mode 100644 index 00000000..c45a2974 --- /dev/null +++ b/.github/workflows/jqmc-run-rc-mixed-precision-pytest.yml @@ -0,0 +1,68 @@ +# An rc test of jqmc. + +name: jqmc rc test + +on: + pull_request: + branches: [ "rc" ] + paths-ignore: + - '.gitignore' + - '.github/**' + - 'doc/**' + - 'examples/**' + - 'benchmarks/**' + - 'README.md' + - '.pre-commit-config.yaml' + - 'jqmc_workflow/**' + +jobs: + run: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - name: Install gfortran and gcc + run: | + sudo apt-get update + sudo apt-get install gfortran + + - name: Install OpenBLAS and LAPACK + run: sudo apt-get install libopenblas-dev liblapack-dev + + - name: Install OpenMPI + run: sudo apt-get install openmpi-bin libopenmpi-dev + + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install jqmc + run: | + python -m pip install flake8 pytest pytest-cov + python -m pip install . + + - name: Test jqmc command-line + run: | + pytest -s -v tests/test_jqmc_command_lines.py + + - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) + run: | + pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed + pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + + - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) + run: | + mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed + mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + + - name: Test jqmc-tool (toolset for jqmc) + run: | + pytest -s -v tests/test_jqmc_tool.py From 8e927839e82bf072eb78537279ecd4f695eddaba Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Tue, 19 May 2026 13:34:41 +0900 Subject: [PATCH 70/97] Transfer ownership to the organization account. --- .github/workflows/jqmc-deploy-test.yml | 2 +- .github/workflows/jqmc-deploy.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/jqmc-deploy-test.yml b/.github/workflows/jqmc-deploy-test.yml index 44319e53..e267c69e 100644 --- a/.github/workflows/jqmc-deploy-test.yml +++ b/.github/workflows/jqmc-deploy-test.yml @@ -5,7 +5,7 @@ on: jobs: deploy-test-pypi: - if: github.repository == 'kousuke-nakano/jQMC' + if: github.repository == 'jqmc-project/jQMC' runs-on: ubuntu-latest diff --git a/.github/workflows/jqmc-deploy.yml b/.github/workflows/jqmc-deploy.yml index 5e219a80..c9eafe8f 100644 --- a/.github/workflows/jqmc-deploy.yml +++ b/.github/workflows/jqmc-deploy.yml @@ -10,7 +10,7 @@ jobs: # validate tag validate_tag: # Run only if this repository is rc - if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'kousuke-nakano/jQMC' + if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'jqmc-project/jQMC' runs-on: ubuntu-latest steps: # Check out the repository @@ -83,7 +83,7 @@ jobs: deploy-pypi: # Run only if this repository is rc needs: validate_tag - if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'kousuke-nakano/jQMC' + if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'jqmc-project/jQMC' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 From f83cdcd6d39d7527685b1090482be3343715fc94 Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Tue, 19 May 2026 13:56:46 +0900 Subject: [PATCH 71/97] Update URLs --- CONTRIBUTING.md | 6 +++--- README.md | 22 +++++++++++----------- doc/conf.py | 4 ++-- doc/examples.md | 2 +- doc/install.md | 2 +- doc/overview.md | 12 ++++++------ setup.cfg | 4 ++-- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c215da6..325bbfc8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,7 +54,7 @@ We are willing to sacrifice some computational speed to achieve these goals. To * Submit a **Pull Request** (PR). * Upon PR creation or update, GitHub Actions will run the test suite. -* If all tests pass, @kousuke-nakano (a main maintainer) will review your changes. +* If all tests pass, @kousuke-nakano or another maintainer of @jqmc-project will review your changes. * Once approved, your PR will be merged into `main`. --- @@ -68,13 +68,13 @@ We are willing to sacrifice some computational speed to achieve these goals. To * `scipy` * `jax` * `flax` -* Other third-party packages should be avoided unless absolutely necessary. Any new dependency must be approved by @kousuke-nakano. +* Other third-party packages should be avoided unless absolutely necessary. Any new dependency must be approved by @kousuke-nakano or another maintainer of @jqmc-project. --- ### Release Process -* All official package releases are performed by @kousuke-nakano as needed. +* All official package releases are performed by @kousuke-nakano or another maintainer of @jqmc-project as needed. --- diff --git a/README.md b/README.md index 45549c8d..a2c59b3d 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ ![jqmc_logo](logo/logo_yoko2.jpg) -**jQMC** is an ab initio quantum Monte Carlo (QMC) simulation package developed entirely from scratch using `Python` and `JAX`. Originally designed for molecular systems --with future extensions planned for periodic systems-- **jQMC** implements two well-established QMC algorithms: Variational Monte Carlo (VMC) and a robust and efficient variant of Diffusion Monte Carlo algorithm known as Lattice Regularized Diffusion Monte Carlo (LRDMC). By leveraging `JAX` just-in-time (`jit`) compilation and vectorized mapping (`vmap`) functionalities, `jQMC` achieves high-performance computations **especially on GPUs** while remaining portable across CPUs and GPUs. See [here](http://jax.readthedocs.io/) for the details of `JAX`. The **jQMC** users and developers manual is available from [GitHub Pages](https://kousuke-nakano.github.io/jQMC/). - -![license](https://img.shields.io/github/license/kousuke-nakano/jQMC) -![tag](https://img.shields.io/github/v/tag/kousuke-nakano/jQMC) -![fork](https://img.shields.io/github/forks/kousuke-nakano/jQMC?style=social) -![stars](https://img.shields.io/github/stars/kousuke-nakano/jQMC?style=social) -![short-pytest](https://github.com/kousuke-nakano/jQMC/actions/workflows/jqmc-run-short-pytest.yml/badge.svg) -![full-pytest](https://github.com/kousuke-nakano/jQMC/actions/workflows/jqmc-run-full-pytest.yml/badge.svg) -![codecov](https://codecov.io/github/kousuke-nakano/jQMC/graph/badge.svg) +**jQMC** is an ab initio quantum Monte Carlo (QMC) simulation package developed entirely from scratch using `Python` and `JAX`. Originally designed for molecular systems --with future extensions planned for periodic systems-- **jQMC** implements two well-established QMC algorithms: Variational Monte Carlo (VMC) and a robust and efficient variant of Diffusion Monte Carlo algorithm known as Lattice Regularized Diffusion Monte Carlo (LRDMC). By leveraging `JAX` just-in-time (`jit`) compilation and vectorized mapping (`vmap`) functionalities, `jQMC` achieves high-performance computations **especially on GPUs** while remaining portable across CPUs and GPUs. See [here](http://jax.readthedocs.io/) for the details of `JAX`. The **jQMC** users and developers manual is available from [GitHub Pages](https://jqmc-project.github.io/jQMC/). + +![license](https://img.shields.io/github/license/jqmc-project/jQMC) +![tag](https://img.shields.io/github/v/tag/jqmc-project/jQMC) +![fork](https://img.shields.io/github/forks/jqmc-project/jQMC?style=social) +![stars](https://img.shields.io/github/stars/jqmc-project/jQMC?style=social) +![short-pytest](https://github.com/jqmc-project/jQMC/actions/workflows/jqmc-run-short-pytest.yml/badge.svg) +![full-pytest](https://github.com/jqmc-project/jQMC/actions/workflows/jqmc-run-full-pytest.yml/badge.svg) +![codecov](https://codecov.io/github/jqmc-project/jQMC/graph/badge.svg) ![DL](https://img.shields.io/pypi/dm/jqmc) ![python_version](https://img.shields.io/pypi/pyversions/jqmc) ![pypi_version](https://badge.fury.io/py/jqmc.svg) @@ -54,7 +54,7 @@ Kosuke Nakano (National Institute for Materials Science (NIMS), Japan) **The latest version of jQMC** can be installed via pip from the cloned GitHub repository. ```bash -% git clone https://github.com/kousuke-nakano/jQMC +% git clone https://github.com/jqmc-project/jQMC % cd jQMC % pip install . ``` @@ -100,7 +100,7 @@ Once the `main` branch is merged into the `rc` branch, the `GitHub` workflow lau ## How to deploy the documentation -Once the `main` branch is merged into the `rc-gh-pages` branch, the `GitHub` workflow launches the implemented documentaion building process (`jqmc-deploy-gh-pages.yml`) and deploy the compiled documentaiton to [GitHub Pages](https://kousuke-nakano.github.io/jQMC/). +Once the `main` branch is merged into the `rc-gh-pages` branch, the `GitHub` workflow launches the implemented documentaion building process (`jqmc-deploy-gh-pages.yml`) and deploy the compiled documentaiton to [GitHub Pages](https://jqmc-project.github.io/jQMC/). ## Contribution diff --git a/doc/conf.py b/doc/conf.py index cbbf5e18..af6fb80a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -50,7 +50,7 @@ def _generate_examples_page(): "# Examples\n" "\n" "Example files for **jQMC** are found at\n" - ".\n" + ".\n" "\n" ) @@ -212,7 +212,7 @@ def _dedup_footnote(m): # documentation. html_theme_options = { "icon_links": [ - {"name": "GitHub", "url": "https://github.com/kousuke-nakano/jQMC", "icon": "fa-brands fa-github"}, + {"name": "GitHub", "url": "https://github.com/jqmc-project/jQMC", "icon": "fa-brands fa-github"}, ], } # html_theme_options = { diff --git a/doc/examples.md b/doc/examples.md index 22191a5e..75be4d7d 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -3,7 +3,7 @@ # Examples Example files for **jQMC** are found at -. +. ## jqmc-example01: diff --git a/doc/install.md b/doc/install.md index 66f07533..b0dd62f5 100644 --- a/doc/install.md +++ b/doc/install.md @@ -2,4 +2,4 @@ # Installation -How to Install **jQMC** is written in `Readme.md` https://github.com/kousuke-nakano/jQMC/tree/main/. +How to Install **jQMC** is written in `Readme.md` https://github.com/jqmc-project/jQMC/tree/main/. diff --git a/doc/overview.md b/doc/overview.md index d7aa2a4b..696b6be3 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -2,12 +2,12 @@ **jQMC** is an ab initio quantum Monte Carlo (QMC) simulation package developed entirely from scratch using Python and JAX. Originally designed for molecular systems--with future extensions planned for periodic systems--**jQMC** implements two well-established QMC algorithms: Variational Monte Carlo (VMC) and a robust and efficient variant of Diffusion Monte Carlo known as Lattice Regularized Diffusion Monte Carlo (LRDMC). By leveraging JAX just-in-time (jit) compilation and vectorized mapping (vmap) functionalities, jQMC achieves high-performance computations especially on GPUs while remaining portable across CPUs and GPUs. -![license](https://img.shields.io/github/license/kousuke-nakano/jQMC) -![tag](https://img.shields.io/github/v/tag/kousuke-nakano/jQMC) -![fork](https://img.shields.io/github/forks/kousuke-nakano/jQMC?style=social) -![stars](https://img.shields.io/github/stars/kousuke-nakano/jQMC?style=social) -![full-pytest](https://github.com/kousuke-nakano/jQMC/actions/workflows/jqmc-run-full-pytest.yml/badge.svg) -![codecov](https://codecov.io/github/kousuke-nakano/jQMC/graph/badge.svg) +![license](https://img.shields.io/github/license/jqmc-project/jQMC) +![tag](https://img.shields.io/github/v/tag/jqmc-project/jQMC) +![fork](https://img.shields.io/github/forks/jqmc-project/jQMC?style=social) +![stars](https://img.shields.io/github/stars/jqmc-project/jQMC?style=social) +![full-pytest](https://github.com/jqmc-project/jQMC/actions/workflows/jqmc-run-full-pytest.yml/badge.svg) +![codecov](https://codecov.io/github/jqmc-project/jQMC/graph/badge.svg) ![DL](https://img.shields.io/pypi/dm/jqmc) ![python_version](https://img.shields.io/pypi/pyversions/jqmc) ![pypi_version](https://badge.fury.io/py/jqmc.svg) diff --git a/setup.cfg b/setup.cfg index 72b5763e..375fea96 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,9 +4,9 @@ author = Kosuke Nakano author_email = kousuke_1123@icloud.com long_description = file: README.md long_description_content_type = text/markdown -url = https://github.com/kousuke-nakano/jQMC +url = https://github.com/jqmc-project/jQMC project_urls = - Bug tracker = https://github.com/kousuke-nakano/jQMC/issues + Bug tracker = https://github.com/jqmc-project/jQMC/issues Documentations = https://jQMC.readthedocs.io/en/latest/ classifiers = Intended Audience :: Science/Research From f5d284493f936abff324550de7d1f58f7b39ab49 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Wed, 20 May 2026 10:38:43 +0900 Subject: [PATCH 72/97] Improve `LRDMC_Ext_Workflow` `LRDMC_Ext_Workflow` now publishes per-`alat` averaged GFMC projections in `output_values["nmpm_per_alat"]` as a list of `{"alat", "nmpm"}` records, matching `per_alat_results`. Its `__init__` method also accepts the same shape for `num_projection_per_measurement` and normalizes it back to `dict[float, int]` internally. --- jqmc_workflow/lrdmc_ext_workflow.py | 56 +++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/jqmc_workflow/lrdmc_ext_workflow.py b/jqmc_workflow/lrdmc_ext_workflow.py index e1444646..7d0c7cc9 100644 --- a/jqmc_workflow/lrdmc_ext_workflow.py +++ b/jqmc_workflow/lrdmc_ext_workflow.py @@ -47,6 +47,7 @@ import subprocess from logging import getLogger +from ._output_parser import parse_lrdmc_output from ._setting import ( GFMC_MIN_BIN_BLOCKS, GFMC_MIN_COLLECT_STEPS, @@ -118,10 +119,19 @@ class LRDMC_Ext_Workflow(Workflow): find its own optimal ``num_projection_per_measurement``. Set to *None* to disable auto-calibration (requires explicit *num_projection_per_measurement*). Activates GFMC_n mode. - num_projection_per_measurement (int, optional): + num_projection_per_measurement (int | dict[float, int] | list[dict], optional): GFMC projections per measurement. When given explicitly, automatic calibration is disabled and this value is used - for every ``alat``. Activates GFMC_n mode. + for every ``alat``. Activates GFMC_n mode. Accepted forms: + + * ``int`` -- the same value for every alat. + * ``dict[float, int]`` -- per-alat values; keys must cover + every alat in ``alat_list`` exactly. + * ``list[dict]`` -- per-alat values as records + ``{"alat": float, "nmpm": int}``. This form is TOML-safe + (no float dict keys) and is the recommended shape when + wired through ``ValueFrom`` from an upstream workflow. + Normalized to the dict form internally. non_local_move (str, optional): Non-local move treatment. Default from ``jqmc_miscs``. E_scf (float, optional): @@ -205,6 +215,12 @@ class LRDMC_Ext_Workflow(Workflow): Statistical error on ``extrapolated_energy`` (Ha). per_alat_results (dict): Per-alat energy/error results keyed by ``alat``. + nmpm_per_alat (list[dict]): + Averaged GFMC projections per alat as records + ``{"alat": float, "nmpm": int}``. Suitable for piping into + a downstream GFMC_n ``LRDMC_Ext_Workflow`` via ``ValueFrom`` + as ``num_projection_per_measurement``. Only present when + sub-run outputs could be parsed. errors (list[str]): Error messages for alat runs that failed. error (str): @@ -287,6 +303,19 @@ def __init__( # None -- GFMC_t mode (uses time_projection_tau) # int -- same value for every alat # dict -- per-alat values; keys must cover every alat in alat_list + # list of {"alat": float, "nmpm": int} -- TOML-safe wire form + # (used by ValueFrom upstream). Normalized to dict here. + if isinstance(num_projection_per_measurement, list): + try: + num_projection_per_measurement = { + float(entry["alat"]): int(entry["nmpm"]) for entry in num_projection_per_measurement + } + except (KeyError, TypeError) as exc: + raise ValueError( + f"num_projection_per_measurement list entries must be " + f"dicts with keys 'alat' and 'nmpm'; got " + f"{num_projection_per_measurement!r}" + ) from exc if isinstance(num_projection_per_measurement, dict): missing = [a for a in self.alat_list if a not in num_projection_per_measurement] if missing: @@ -473,6 +502,29 @@ async def _run_one(enc): return self.status, [], {"error": msg} self.output_values["per_alat_results"] = per_alat_results + + # Publish averaged GFMC projections per alat as a TOML-safe + # list of {"alat": float, "nmpm": int} records. A downstream + # GFMC_n LRDMC_Ext_Workflow can consume this via ValueFrom and + # pass it back as num_projection_per_measurement (the __init__ + # accepts this list form and normalizes to dict[float, int]). + nmpm_per_alat: list[dict] = [] + for alat in self.alat_list: + alat_dir = os.path.join(self.project_dir, f"lrdmc_alat_{alat:.3f}") + try: + diag = parse_lrdmc_output(alat_dir) + except Exception: + diag = None + if diag is not None and getattr(diag, "avg_num_projections", None) is not None: + nmpm_per_alat.append( + { + "alat": float(alat), + "nmpm": max(int(round(float(diag.avg_num_projections))), 1), + } + ) + if nmpm_per_alat: + self.output_values["nmpm_per_alat"] = nmpm_per_alat + self.output_files = restart_chks self.status = WorkflowStatus.COMPLETED return self.status, self.output_files, self.output_values From 5adef863efc4d758dbb313b3569c0df8572e145c Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Wed, 20 May 2026 10:51:47 +0900 Subject: [PATCH 73/97] Fix `LRDMC_Ext_Workflow` again. --- jqmc_workflow/lrdmc_ext_workflow.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/jqmc_workflow/lrdmc_ext_workflow.py b/jqmc_workflow/lrdmc_ext_workflow.py index 7d0c7cc9..02602b14 100644 --- a/jqmc_workflow/lrdmc_ext_workflow.py +++ b/jqmc_workflow/lrdmc_ext_workflow.py @@ -426,6 +426,23 @@ async def run(self) -> tuple: """ self._ensure_project_dir() _wd = self.project_dir + + # When num_projection_per_measurement comes through ValueFrom, + # the launcher resolves it via setattr after __init__, so the + # __init__-time list-of-records normalization is bypassed. + # Re-apply it here so _make_lrdmc_workflow sees a dict. + if isinstance(self.num_projection_per_measurement, list): + try: + self.num_projection_per_measurement = { + float(entry["alat"]): int(entry["nmpm"]) for entry in self.num_projection_per_measurement + } + except (KeyError, TypeError) as exc: + raise ValueError( + f"num_projection_per_measurement list entries must be " + f"dicts with keys 'alat' and 'nmpm'; got " + f"{self.num_projection_per_measurement!r}" + ) from exc + sorted_alats = sorted(self.alat_list, reverse=True) # -- helper: run a single alat, return a uniform result tuple ------ From 9cdedacc33423c582e0be30f38de4f63c85834bc Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 21 May 2026 16:26:54 +0900 Subject: [PATCH 74/97] Improve GFMC_n on-the-fly `E_scf` update, leaving bad-regime samples in permanent contamination (#78) * Update docs. * Optimize J2 ratio: O(N^2) baseline -> O(N * N_grid) per-grid sums * Introduce J3 state carry in MCMC update. * Implement the slim J3 carry for MCMC WF update. * Refactor atomic_orbital: coalesce primitive gather and lift r-R to unique-atom rank Address the L1 LSU wavefront-pipe bottleneck (~90% of peak) identified in `_compute_AOs_cart` `_compute_AOs_sphe` and their Laplacians. Coalesced bucket reduction: - Introduce `PrimBucketLayout` (NamedTuple) and rewrite `_build_prim_buckets_by_K` to return a permanent bucket-K-major permutation of the primitive axis. - Apply the permutation inside every `_*_prim_*` accessor on `AOs_cart_data` / `AOs_sphe_data`, so all primitive arrays are stored bucket-K-major. - `_reduce_primitives_to_aos` now reduces each K bucket via `dynamic_slice + reshape + reduce_sum` instead of a fancy primitive-index gather, collapsing L1 sectors-per-request from ~10 toward the ideal of 4. r-R at unique-atom rank: - Compute `r-R` and `r^2` at unique-atom rank (`n_atoms_unique << num_ao_prim`) and gather to primitive rank via `_nucleus_index_prim_jnp`. - Eliminates the `(num_ao_prim, n_elec, 3)` broadcast intermediate that drove most of the register-spill local-memory traffic. - Applied to `_compute_AOs_cart`, `_compute_AOs_sphe`, `_compute_AOs_laplacian_analytic_cart`, and `_compute_AOs_laplacian_analytic_sphe` (the spherical Laplacian additionally computes `grad_S . r_R` at unique rank). * Fixed a trivial bug atomic_orbital: permute primitive arrays along the last axis `_exponents_jnp` / `_coefficients_jnp` applied the bucket-K-major permutation via `arr[self._prim_perm_np]`, which targets axis 0. This is fine for the AO eval kernels (1D primitive arrays), but `collect_param_grads` reads these accessors on a vmap-batched `AOs_*_data` whose `exponents` / `coefficients` leaves are 2D `(num_walkers, num_ao_prim)`, the take then mangled the walker axis into the primitive axis and produced shape mismatches downstream. Switch to `jnp.take(arr, self._prim_perm_np, axis=-1)` so the permutation is always applied to the primitive axis regardless of prepended batch dims. AO-eval results are bit-exact identical for the 1D inputs used by `_compute_AOs_*`. * Fixed a bug in AOs. return raw-order exponents/coefficients from public accessors The public ao_exponents/ao_coefficients on Geminal_data and Jastrow_three_body_data were leaking the bucket-K-major permuted form (_exponents_jnp/_coefficients_jnp), which broke round-trips through the basis-optimization API (ShellPrimMap.symmetrize, with_updated_ao_*, apply_block_update) and produced double-mangled values. Public accessors now return basis-natural order via jnp.asarray(self.exponents/coefficients). The bucket-K permutation stays strictly inside the AO eval kernel boundary. Also revert the jnp.take(..., axis=-1) workaround introduced in 561ca40 in atomic_orbital._{exponents,coefficients}_jnp. * Revert "Fixed a bug in AOs. return raw-order exponents/coefficients from public accessors" This reverts commit f38dd2193183f0d77806433c0a6bfdf53714ba72. * Revert "Fixed a trivial bug atomic_orbital: permute primitive arrays along the last axis" This reverts commit 561ca4087ddd77b02deee9d13d467be543aa013f. * Revert "Refactor atomic_orbital: coalesce primitive gather and lift r-R to unique-atom rank" This reverts commit 43bfa00287b3ffe80711b8128ab9beeba511419d. * Improve GFMC_n on-the-fly `E_scf` update, leaving bad-regime samples in permanent contamination The on-the-fly `E_scf` update in `GFMC_n` / `_GFMC_n_debug` was gated by `(i_mcmc_step + 1) % mcmc_interval == 0`, which delayed the first update until iteration `eq_steps + mcmc_interval - 1`. Since `mcmc_interval = num_mcmc_steps / 100`, the first-update step grew linearly with `N`. As a result, `stored_w_L` values projected with the possibly far-off initial `E_scf` were pinned as permanent K-window-product outliers in `__G_L`, and the on-the-fly jackknife never recovered as `N` grew: sigma stayed flat instead of falling as `1/sqrt(N)`. The same outlier-laden tail also leaked into the final `get_E()` mixed estimator whenever the user-set `num_gfmc_warmup_steps` was smaller than the run-dependent bad-regime length. This was introduced in `c287272` (2025-02-05), when the `E_scf` update logic was first added inside the display-throttle block. Fix: - Remove the `mcmc_interval` throttle from the update logic so that `E_scf` is updated every step during the rapid phase (`i_mcmc_step < mcmc_interval`) and every `mcmc_interval` steps thereafter. The MPI cost increase is bounded by about `N / 100` extra broadcasts, which is negligible relative to projection time. - Derive the jackknife warmup cap from the actual bad-regime boundary: `GFMC_ON_THE_FLY_COLLECT_STEPS + GFMC_ON_THE_FLY_BIN_BLOCKS` (`= 20` with the current constants), instead of the magic `eq_steps = 20` - Remove `GFMC_ON_THE_FLY_WARMUP_STEPS` from `_setting.py` because it is no longer used; it is replaced by `mcmc_interval` as the rapid/throttle boundary. --- jqmc/_setting.py | 1 - jqmc/jqmc_gfmc.py | 105 ++++++++++++++++++++++++++-------------------- 2 files changed, 60 insertions(+), 46 deletions(-) diff --git a/jqmc/_setting.py b/jqmc/_setting.py index ef6ea535..2e0f8874 100644 --- a/jqmc/_setting.py +++ b/jqmc/_setting.py @@ -56,7 +56,6 @@ GFMC_MIN_COLLECT_STEPS = 5 # on the fly statistics param -GFMC_ON_THE_FLY_WARMUP_STEPS = 20 GFMC_ON_THE_FLY_COLLECT_STEPS = 10 GFMC_ON_THE_FLY_BIN_BLOCKS = 10 diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index ab86d865..1260133f 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -63,7 +63,6 @@ GFMC_MIN_WARMUP_STEPS, GFMC_ON_THE_FLY_BIN_BLOCKS, GFMC_ON_THE_FLY_COLLECT_STEPS, - GFMC_ON_THE_FLY_WARMUP_STEPS, get_eps, ) from .coulomb_potential import ( @@ -6376,10 +6375,10 @@ def _compute_local_energy_n( start_update_E_scf = time.perf_counter() ## parameters for E_scf - eq_steps = GFMC_ON_THE_FLY_WARMUP_STEPS num_gfmc_collect_steps = GFMC_ON_THE_FLY_COLLECT_STEPS num_gfmc_bin_blocks = GFMC_ON_THE_FLY_BIN_BLOCKS + # (A) accumulate __G_L / __G_e_L every step (after enough stored_w_L) if mpi_rank == 0: if i_mcmc_step >= num_gfmc_collect_steps: e_L = self.__stored_e_L[self.__mcmc_counter + num_mcmc_done] @@ -6390,40 +6389,46 @@ def _compute_local_energy_n( self.__G_L.append(G_L) self.__G_e_L.append(G_L * e_L) - if (i_mcmc_step + 1) % mcmc_interval == 0: - if i_mcmc_step > eq_steps: - if mpi_rank == 0: - num_gfmc_warmup_steps = np.minimum(eq_steps, i_mcmc_step - eq_steps) - logger.debug(f" Computing E_scf at step {i_mcmc_step}.") - G_eq = np.array(self.__G_L[num_gfmc_warmup_steps:]) - G_e_L_eq = np.array(self.__G_e_L[num_gfmc_warmup_steps:]) - G_e_L_split = np.array_split(G_e_L_eq, num_gfmc_bin_blocks) - G_e_L_binned = np.array([np.sum(G_e_L_list) for G_e_L_list in G_e_L_split]) - G_split = np.array_split(G_eq, num_gfmc_bin_blocks) - G_binned = np.array([np.sum(G_list) for G_list in G_split]) - G_e_L_binned_sum = np.sum(G_e_L_binned) - G_binned_sum = np.sum(G_binned) - E_jackknife = [ - (G_e_L_binned_sum - G_e_L_binned[m]) / (G_binned_sum - G_binned[m]) - for m in range(num_gfmc_bin_blocks) - ] - E_mean = np.average(E_jackknife) - E_std = np.sqrt(num_gfmc_bin_blocks - 1) * np.std(E_jackknife) - E_mean = float(E_mean) - E_std = float(E_std) - else: - E_mean = None - E_std = None - - E_mean = mpi_comm.bcast(E_mean, root=0) - E_std = mpi_comm.bcast(E_std, root=0) + # (B) E_scf update schedule: + # - rapid phase (i_mcmc_step < mcmc_interval = N/100): update every step + # - thereafter: update every mcmc_interval steps + # - skip when there are not yet enough G_L samples for jackknife + n_G_L = max(0, i_mcmc_step - num_gfmc_collect_steps + 1) + have_enough = n_G_L >= num_gfmc_bin_blocks + in_rapid_phase = i_mcmc_step < mcmc_interval + on_throttle = (i_mcmc_step + 1) % mcmc_interval == 0 + + if have_enough and (in_rapid_phase or on_throttle): + if mpi_rank == 0: + # Skip the bad-regime stored_w_L that bleed into __G_L via the K-product. + # During the very early phase the available samples are limited, so + # clamp to keep at least num_gfmc_bin_blocks samples for jackknife. + num_gfmc_warmup_steps = min( + num_gfmc_collect_steps + num_gfmc_bin_blocks, + max(n_G_L - num_gfmc_bin_blocks, 0), + ) + G_eq = np.array(self.__G_L[num_gfmc_warmup_steps:]) + G_e_L_eq = np.array(self.__G_e_L[num_gfmc_warmup_steps:]) + G_e_L_split = np.array_split(G_e_L_eq, num_gfmc_bin_blocks) + G_e_L_binned = np.array([np.sum(G_e_L_list) for G_e_L_list in G_e_L_split]) + G_split = np.array_split(G_eq, num_gfmc_bin_blocks) + G_binned = np.array([np.sum(G_list) for G_list in G_split]) + G_e_L_binned_sum = np.sum(G_e_L_binned) + G_binned_sum = np.sum(G_binned) + E_jackknife = [ + (G_e_L_binned_sum - G_e_L_binned[m]) / (G_binned_sum - G_binned[m]) for m in range(num_gfmc_bin_blocks) + ] + E_mean = float(np.average(E_jackknife)) + E_std = float(np.sqrt(num_gfmc_bin_blocks - 1) * np.std(E_jackknife)) + else: + E_mean = None + E_std = None - self.__E_scf = E_mean - E_scf_std = E_std + E_mean = mpi_comm.bcast(E_mean, root=0) + E_std = mpi_comm.bcast(E_std, root=0) - logger.debug(f" Updated E_scf = {self.__E_scf:.5f} +- {E_scf_std:.5f} Ha.") - else: - logger.debug(f" Init E_scf = {self.__E_scf:.5f} Ha. Being equilibrated.") + self.__E_scf = E_mean + logger.debug(f" E_scf = {self.__E_scf:.5f} +- {E_std:.5f} Ha.") mpi_comm.Barrier() end_update_E_scf = time.perf_counter() @@ -8224,19 +8229,29 @@ def _compute_local_energy_n_debug( self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=jnp.float64) # update E_scf - eq_steps = GFMC_ON_THE_FLY_WARMUP_STEPS num_gfmc_collect_steps = GFMC_ON_THE_FLY_COLLECT_STEPS num_gfmc_bin_blocks = GFMC_ON_THE_FLY_BIN_BLOCKS - if (i_mcmc_step + 1) % mcmc_interval == 0: - if i_mcmc_step > eq_steps: - self.__E_scf, E_scf_std = self.get_E_on_the_fly( - num_gfmc_warmup_steps=np.minimum(eq_steps, i_mcmc_step - eq_steps), - num_gfmc_bin_blocks=num_gfmc_bin_blocks, - num_gfmc_collect_steps=num_gfmc_collect_steps, - ) - logger.debug(f" Updated E_scf = {self.__E_scf:.5f} +- {E_scf_std:.5f} Ha.") - else: - logger.debug(f" Init E_scf = {self.__E_scf:.5f} Ha. Being equilibrated.") + + # E_scf update schedule: + # - rapid phase (i_mcmc_step < mcmc_interval = N/100): update every step + # - thereafter: update every mcmc_interval steps + # - skip when there are not yet enough G_L samples for jackknife + n_G_L = max(0, i_mcmc_step - num_gfmc_collect_steps + 1) + have_enough = n_G_L >= num_gfmc_bin_blocks + in_rapid_phase = i_mcmc_step < mcmc_interval + on_throttle = (i_mcmc_step + 1) % mcmc_interval == 0 + + if have_enough and (in_rapid_phase or on_throttle): + num_gfmc_warmup_steps = min( + num_gfmc_collect_steps + num_gfmc_bin_blocks, + max(n_G_L - num_gfmc_bin_blocks, 0), + ) + self.__E_scf, E_scf_std = self.get_E_on_the_fly( + num_gfmc_warmup_steps=num_gfmc_warmup_steps, + num_gfmc_bin_blocks=num_gfmc_bin_blocks, + num_gfmc_collect_steps=num_gfmc_collect_steps, + ) + logger.debug(f" E_scf = {self.__E_scf:.5f} +- {E_scf_std:.5f} Ha.") # count up, here is the end of the branching step. num_mcmc_done += 1 From f74e13c90cf01d768acee24308b09b1a059dafc4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 23:11:06 +0900 Subject: [PATCH 75/97] [pre-commit.ci] pre-commit autoupdate (#76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.12 → v0.15.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.12...v0.15.13) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04a2d812..067339fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.12 + rev: v0.15.13 hooks: - id: ruff name: ruff (ambiguous unicode only) From 70f3b46093366f5fbb9f01d7e5e40f3b76eed3b7 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Mon, 25 May 2026 23:37:42 +0900 Subject: [PATCH 76/97] LM SR fallback, distributed tall-CG, GFMC_n E_scf, workflow polish (#79) * Update docs. * Optimize J2 ratio: O(N^2) baseline -> O(N * N_grid) per-grid sums * Introduce J3 state carry in MCMC update. * Implement the slim J3 carry for MCMC WF update. * Refactor atomic_orbital: coalesce primitive gather and lift r-R to unique-atom rank Address the L1 LSU wavefront-pipe bottleneck (~90% of peak) identified in `_compute_AOs_cart` `_compute_AOs_sphe` and their Laplacians. Coalesced bucket reduction: - Introduce `PrimBucketLayout` (NamedTuple) and rewrite `_build_prim_buckets_by_K` to return a permanent bucket-K-major permutation of the primitive axis. - Apply the permutation inside every `_*_prim_*` accessor on `AOs_cart_data` / `AOs_sphe_data`, so all primitive arrays are stored bucket-K-major. - `_reduce_primitives_to_aos` now reduces each K bucket via `dynamic_slice + reshape + reduce_sum` instead of a fancy primitive-index gather, collapsing L1 sectors-per-request from ~10 toward the ideal of 4. r-R at unique-atom rank: - Compute `r-R` and `r^2` at unique-atom rank (`n_atoms_unique << num_ao_prim`) and gather to primitive rank via `_nucleus_index_prim_jnp`. - Eliminates the `(num_ao_prim, n_elec, 3)` broadcast intermediate that drove most of the register-spill local-memory traffic. - Applied to `_compute_AOs_cart`, `_compute_AOs_sphe`, `_compute_AOs_laplacian_analytic_cart`, and `_compute_AOs_laplacian_analytic_sphe` (the spherical Laplacian additionally computes `grad_S . r_R` at unique rank). * Fixed a trivial bug atomic_orbital: permute primitive arrays along the last axis `_exponents_jnp` / `_coefficients_jnp` applied the bucket-K-major permutation via `arr[self._prim_perm_np]`, which targets axis 0. This is fine for the AO eval kernels (1D primitive arrays), but `collect_param_grads` reads these accessors on a vmap-batched `AOs_*_data` whose `exponents` / `coefficients` leaves are 2D `(num_walkers, num_ao_prim)`, the take then mangled the walker axis into the primitive axis and produced shape mismatches downstream. Switch to `jnp.take(arr, self._prim_perm_np, axis=-1)` so the permutation is always applied to the primitive axis regardless of prepended batch dims. AO-eval results are bit-exact identical for the 1D inputs used by `_compute_AOs_*`. * Fixed a bug in AOs. return raw-order exponents/coefficients from public accessors The public ao_exponents/ao_coefficients on Geminal_data and Jastrow_three_body_data were leaking the bucket-K-major permuted form (_exponents_jnp/_coefficients_jnp), which broke round-trips through the basis-optimization API (ShellPrimMap.symmetrize, with_updated_ao_*, apply_block_update) and produced double-mangled values. Public accessors now return basis-natural order via jnp.asarray(self.exponents/coefficients). The bucket-K permutation stays strictly inside the AO eval kernel boundary. Also revert the jnp.take(..., axis=-1) workaround introduced in 561ca40 in atomic_orbital._{exponents,coefficients}_jnp. * Revert "Fixed a bug in AOs. return raw-order exponents/coefficients from public accessors" This reverts commit f38dd2193183f0d77806433c0a6bfdf53714ba72. * Revert "Fixed a trivial bug atomic_orbital: permute primitive arrays along the last axis" This reverts commit 561ca4087ddd77b02deee9d13d467be543aa013f. * Revert "Refactor atomic_orbital: coalesce primitive gather and lift r-R to unique-atom rank" This reverts commit 43bfa00287b3ffe80711b8128ab9beeba511419d. * Improve GFMC_n on-the-fly `E_scf` update, leaving bad-regime samples in permanent contamination The on-the-fly `E_scf` update in `GFMC_n` / `_GFMC_n_debug` was gated by `(i_mcmc_step + 1) % mcmc_interval == 0`, which delayed the first update until iteration `eq_steps + mcmc_interval - 1`. Since `mcmc_interval = num_mcmc_steps / 100`, the first-update step grew linearly with `N`. As a result, `stored_w_L` values projected with the possibly far-off initial `E_scf` were pinned as permanent K-window-product outliers in `__G_L`, and the on-the-fly jackknife never recovered as `N` grew: sigma stayed flat instead of falling as `1/sqrt(N)`. The same outlier-laden tail also leaked into the final `get_E()` mixed estimator whenever the user-set `num_gfmc_warmup_steps` was smaller than the run-dependent bad-regime length. This was introduced in `c287272` (2025-02-05), when the `E_scf` update logic was first added inside the display-throttle block. Fix: - Remove the `mcmc_interval` throttle from the update logic so that `E_scf` is updated every step during the rapid phase (`i_mcmc_step < mcmc_interval`) and every `mcmc_interval` steps thereafter. The MPI cost increase is bounded by about `N / 100` extra broadcasts, which is negligible relative to projection time. - Derive the jackknife warmup cap from the actual bad-regime boundary: `GFMC_ON_THE_FLY_COLLECT_STEPS + GFMC_ON_THE_FLY_BIN_BLOCKS` (`= 20` with the current constants), instead of the magic `eq_steps = 20` - Remove `GFMC_ON_THE_FLY_WARMUP_STEPS` from `_setting.py` because it is no longer used; it is replaced by `mcmc_interval` as the rapid/throttle boundary. * Fix continuation step overestimation in LRDMC/MCMC workflows `accumulated_measurement` was being incremented by the planned `estimated_steps - warmup` after every `_submit_and_wait`, regardless of whether the run actually completed all planned branching steps. When a GFMC run was cut short by `max_time` ("Break the branching loop"), the workflow overcounted the accumulated samples, inflating `estimate_additional_steps` and causing the target step count to grow on each continuation. * Improve jqmc-workflow behaviors. - Reconcile orphan `submitted`/`completed` jobs to `fetched` on re-entry - Always use `jqmc-tool` for energy (drop stdout `%.5f` fast-path) - Cache (energy, error) in `[estimation]` keyed on accumulated steps + post-proc params - Allow re-entry into `completed` LRDMC/MCMC when target_error is not yet met * Clean up jqmc-workflow. Solved many trivial bugs. * Improve jqmc-workflow error reporting and fix silent failures - `launcher`: include the per-job reason and path in the DAG execution summary so failed runs are easier to diagnose - `vmc_workflow`: detect non-finite energies at each optimization step by parsing before `validate_completion`, and consolidate VMC log parsing through `_output_parser._parse_vmc_log_text` - `_output_parser`: extend the energy / force / SNR regexes to match `nan` / `inf` so diverged runs surface as non-finite floats instead of being silently dropped from convergence checks - `lrdmc_ext_workflow`: read `num_projection_per_measurement` from each sub-workflow's `output_values` (`GFMC_n` / `lrdmc-bra` path) before falling back to the `GFMC_t` output-file diagnostic, which does not exist in bra mode * Add `|v_0|^2 < 0.9` fallback to plain SR in LM optimization `solve_linear_method` now returns `v0_sq_best` alongside `(c_vec, E_lm)`. The LM caller falls back to `theta = 0.1 * g_sr` when the selected eigenvector's overlap with the current wavefunction is small, preventing NaN energies from updates that lie outside the linear regime. This also removes the now-redundant `|v_0|^2 < 0.01` warning inside the solver. Tests and `Debug_MCMC.solve_linear_method` have been updated for the new return value. * Make tall CG SR solve distributed: `psum` instead of `all_gather(X)` Per-rank memory no longer scales with `mpi_size`, fixing weak-scaling OOM. Keep the legacy kernel as dead code pending verification on real hardware. * Remove legacy tall-CG kernel; promote distributed version to canonical name The distributed `psum`-based tall-CG kernel has now been verified: it eliminates the OOM issue and preserves results. Remove the legacy `all_gather`-based kernel, its driver method, and the equivalence test, and rename the distributed implementation to the canonical names. Net behavior is unchanged from the prior commit. * LRDMC_Workflow: publish averaged nmpm in GFMC_t mode Expose parsed `avg_num_projections` as `output_values["num_projection_per_measurement"]` in tau mode, matching the `GFMC_n` key. This simplifies ` --- doc/examples.md | 6 + jqmc/jqmc_mcmc.py | 182 ++++++++--- jqmc_workflow/_cli.py | 38 ++- jqmc_workflow/_error_estimator.py | 44 +++ jqmc_workflow/_input_generator.py | 19 +- jqmc_workflow/_job.py | 7 +- jqmc_workflow/_machine.py | 105 +++++-- jqmc_workflow/_output_parser.py | 42 ++- jqmc_workflow/_phase.py | 8 +- jqmc_workflow/_state.py | 209 ++++++++++--- jqmc_workflow/_transfer.py | 35 ++- jqmc_workflow/launcher.py | 98 +++++- jqmc_workflow/lrdmc_ext_workflow.py | 128 ++++++-- jqmc_workflow/lrdmc_workflow.py | 373 ++++++++++++++++------- jqmc_workflow/mcmc_workflow.py | 307 +++++++++++++------ jqmc_workflow/template/machine_data.yaml | 27 +- jqmc_workflow/vmc_workflow.py | 124 +++++--- jqmc_workflow/wf_workflow.py | 16 +- jqmc_workflow/workflow.py | 209 +++++++++++-- tests/test_jqmc_mcmc.py | 20 +- 20 files changed, 1521 insertions(+), 476 deletions(-) diff --git a/doc/examples.md b/doc/examples.md index 75be4d7d..9c01c816 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -2510,12 +2510,18 @@ This example uses local execution (`jqmc_setting_local/`): localhost: machine_type: local queuing: false + jobsubmit: bash # required even for queuing=false: command used to invoke the submit script + # (use "bash" or "sh"; for queuing=true add jobcheck / jobdel / jobnum_index) ## Remote machines require ssh_host: ## cluster: ## ssh_host: my-cluster # Host alias in ~/.ssh/config ## machine_type: remote ## queuing: true +## jobsubmit: qsub # required: scheduler submit command (qsub / sbatch / ...) +## jobcheck: qstat # required when queuing=true: status query command +## jobdel: qdel # required when queuing=true: cancel command +## jobnum_index: 0 # required when queuing=true: index of the job-id token in jobsubmit's stdout ## ... ``` diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index 6d67ad02..785f1935 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -133,7 +133,9 @@ def _loglevel_devel(self, message, *args, **kwargs): # - wide + direct : (X X^T + eps I) y = X F via psum # - wide + CG : same system, conjugate gradient with psum'd matvec # - tall + direct : (X^T X + eps I) z = F, theta = X z via all_gather -# - tall + CG : same system, conjugate gradient on replicated inputs +# - tall + CG : same system, conjugate gradient via psum on +# sharded (N_local,) state -- ``X`` stays sharded so +# per-rank memory is independent of ``mpi_size``. # # Compiled kernels are cached at module level so the JIT cost is paid once # per process. The same code path runs on: @@ -198,6 +200,60 @@ def body(state): return x_f, jnp.sqrt(rs_f), k_f +def _cg_while_loop_sharded(b, apply_A, x0, max_iter, tol, dtype, axis_name): + """``_cg_while_loop`` variant for vectors sharded along ``axis_name``. + + All vectors (``b``, ``x0`` and the output of ``apply_A``) live in + the per-rank slice ``(N_local,)``; inner products are summed + globally with ``jax.lax.psum`` so the loop's convergence check and + step-size scalars match the replicated (un-sharded) implementation + bit-for-bit when only the round-off order changes. + + This helper is what makes the "distributed" CG kernel below feasible + without materialising the full design matrix on every rank. + """ + tiny = jnp.asarray(jnp.finfo(dtype).tiny, dtype=dtype) + tol_sq = tol * tol + + def gdot(a, c): + # Global inner product across the sharded axis. + return jax.lax.psum(jnp.dot(a, c), axis_name) + + r0 = b - apply_A(x0) + rs0 = gdot(r0, r0) + state0 = ( + x0, + r0, + r0, # p + rs0, + jnp.int32(0), + jnp.bool_(False), # breakdown + ) + + def cond(state): + _x, _r, _p, rs, k, breakdown = state + return (k < max_iter) & (rs > tol_sq) & jnp.logical_not(breakdown) + + def body(state): + x, r, p, rs_old, k, breakdown = state + Ap = apply_A(p) + denom = gdot(p, Ap) + new_breakdown = breakdown | jnp.logical_not(jnp.isfinite(denom)) | (jnp.abs(denom) <= tiny) + safe_denom = jnp.where(new_breakdown, jnp.asarray(1.0, dtype=dtype), denom) + alpha = rs_old / safe_denom + x_new = jnp.where(new_breakdown, x, x + alpha * p) + r_new = jnp.where(new_breakdown, r, r - alpha * Ap) + rs_new_real = gdot(r_new, r_new) + rs_new = jnp.where(new_breakdown, rs_old, rs_new_real) + safe_rs_old = jnp.where(rs_old > 0, rs_old, jnp.asarray(1.0, dtype=dtype)) + beta = rs_new / safe_rs_old + p_new = jnp.where(new_breakdown, p, r_new + beta * p) + return (x_new, r_new, p_new, rs_new, k + 1, new_breakdown) + + x_f, _r_f, _p_f, rs_f, k_f, _bk_f = jax.lax.while_loop(cond, body, state0) + return x_f, jnp.sqrt(rs_f), k_f + + def _get_sr_wide_direct_kernel(): """Wide-matrix direct SR solve: ``theta = (X X^T + eps I)^{-1} (X F)``. @@ -304,7 +360,21 @@ def _solve(X, F, epsilon): def _get_sr_tall_cg_kernel(): - """Tall-matrix CG SR solve via push-through identity.""" + """Tall-matrix CG SR solve via push-through identity. + + ``X`` stays sharded as ``(P, N_local)`` on every rank; the only + communications are + + * ``psum`` of ``X v_local`` to form the ``(P,)`` projection used by + ``apply_A``; + * ``psum`` of the CG inner products (scalars); + * ``psum`` of ``X y_local`` to assemble the final ``theta``; + * one ``all_gather`` of ``y_local`` (size ``N_total``, vector) so + the returned warm-start dual has the canonical replicated shape. + + Per-rank peak memory therefore stays proportional to ``P * N_local`` + plus ``O(P) + O(N_total)`` scratch, independent of ``mpi_size``. + """ cached = getattr(_get_sr_tall_cg_kernel, "_cached", None) if cached is not None: return cached @@ -323,20 +393,38 @@ def _get_sr_tall_cg_kernel(): PSpec(), # tol PSpec(), # x0 (N_total,) replicated ), + # All four outputs replicated: theta (P,) via psum; y (N_total,) + # via final all_gather; residual and num_iter are scalars. out_specs=(PSpec(), PSpec(), PSpec(), PSpec()), - check_vma=False, # all_gather output is replicated but not statically inferrable + check_vma=False, # final all_gather output is replicated but not statically inferrable ) def _solve(X, F, epsilon, max_iter, tol, x0): - X_full = jax.lax.all_gather(X, "rank", axis=1, tiled=True) - F_full = jax.lax.all_gather(F, "rank", tiled=True) + # x0 arrives replicated as (N_total,); slice to this rank's + # contiguous chunk so the CG state lives in (N_local,) form. + n_local = F.shape[0] + rank_idx = jax.lax.axis_index("rank") + x0_local = jax.lax.dynamic_slice(x0, (rank_idx * n_local,), (n_local,)) - def apply_A(v): - return X_full.T @ (X_full @ v) + epsilon * v + def apply_A(v_local): + # v_local : (N_local,) sharded on "rank" + # X v -- accumulate per-rank contributions of the column-sharded matmul. + u = jax.lax.psum(X @ v_local, "rank") # (P,) replicated + # X^T u -- replicated u dotted with this rank's X columns gives the + # local slice of the result; no further collective needed. + return X.T @ u + epsilon * v_local # (N_local,) + + y_local, residual, num_iter = _cg_while_loop_sharded(F, apply_A, x0_local, max_iter, tol, X.dtype, axis_name="rank") + + # theta = X y -- assembled via psum of per-rank X_local @ y_local. + theta = jax.lax.psum(X @ y_local, "rank") # (P,) replicated + + # Gather y to (N_total,) replicated so the host-side warm-start + # dual stored by the caller is the canonical full-sample vector. + # This is a (N_total,)-sized broadcast -- negligible vs the + # (P, N_total) we would otherwise pay to replicate X. + y_full = jax.lax.all_gather(y_local, "rank", tiled=True) - y, residual, num_iter = _cg_while_loop(F_full, apply_A, x0, max_iter, tol, X.dtype) - # Return y (sample-space CG solution) too so the caller can persist - # it as a warm-start for the next optimization step. - return X_full @ y, y, residual, num_iter + return theta, y_full, residual, num_iter _get_sr_tall_cg_kernel._cached = _solve return _solve @@ -2479,7 +2567,7 @@ def solve_linear_method( K_matrix: npt.NDArray, B_matrix: npt.NDArray, epsilon: float, - ) -> tuple[npt.NDArray, float]: + ) -> tuple[npt.NDArray, float, float]: r"""Solve the Linear Method generalized eigenvalue problem. Constructs extended matrices :math:`\bar H` and :math:`\bar S` of @@ -2487,7 +2575,9 @@ def solve_linear_method( and solves :math:`\bar H v = E \bar S v`. The eigenvector with the largest :math:`|v_0|^2` is selected, and the - parameter update is :math:`c_k = v_k / v_0`. + parameter update is :math:`c_k = v_k / v_0`. :math:`|v_0|^2` is also + returned so the caller can guard against updates that fall outside + the linear regime (small overlap with the current wavefunction). Args: H_0: Current energy :math:`E_\alpha`. @@ -2498,8 +2588,10 @@ def solve_linear_method( epsilon: Eigenvalue cutoff for S matrix. Returns: - tuple: ``(c_vec, E_lm)`` where ``c_vec`` has shape ``(p,)`` - (in the original parameter space) and ``E_lm`` is the selected eigenvalue. + tuple: ``(c_vec, E_lm, v0_sq_best)`` where ``c_vec`` has shape + ``(p,)`` (in the original parameter space), ``E_lm`` is the + selected eigenvalue, and ``v0_sq_best`` is the :math:`|v_0|^2` + of the selected eigenvector (in [0, 1]). """ p = len(f_vec) @@ -2526,7 +2618,7 @@ def solve_linear_method( if not np.any(alive): logger.warning(" LM dgelscut: all parameters removed in Step 1; returning zero update.") - return np.zeros(p, dtype=dtype_mcmc_np), H_0 + return np.zeros(p, dtype=dtype_mcmc_np), H_0, 0.0 # ---- Step 2: Build correlation matrix for alive parameters ---- alive_idx = np.where(alive)[0] @@ -2539,7 +2631,7 @@ def solve_linear_method( n_alive = len(idx) if n_alive == 0: logger.warning(" LM dgelscut: all parameters removed; returning zero update.") - return np.zeros(p, dtype=dtype_mcmc_np), H_0 + return np.zeros(p, dtype=dtype_mcmc_np), H_0, 0.0 # Build correlation matrix for current alive set D_sub = D_inv_sqrt[idx] # (n_alive,) @@ -2599,7 +2691,7 @@ def solve_linear_method( if p_prime == 0: logger.warning(" LM: no positive S eigenvalues after dgelscut; returning zero update.") - return np.zeros(p, dtype=dtype_mcmc_np), H_0 + return np.zeros(p, dtype=dtype_mcmc_np), H_0, 0.0 # P = U Lambda^{-1/2} (S-orthonormal basis) inv_sqrt_Lambda = 1.0 / np.sqrt(Lambda) @@ -2623,9 +2715,15 @@ def solve_linear_method( eigvals_lm, eigvecs_lm = np.linalg.eigh(H_bar) # ---- Select eigenvector with max |v_0|^2 ---- + # |v_0|^2 measures the overlap with the current wavefunction; large + # overlap means the LM step stays in the linear regime. The caller + # is expected to reject the update (e.g. fall back to plain SR) when + # ``v0_sq_best`` is below its safety threshold -- this routine only + # surfaces the value, it does not enforce a cutoff. v0_sq = eigvecs_lm[0, :] ** 2 best_idx = int(np.argmax(v0_sq)) E_lm = float(eigvals_lm[best_idx]) + v0_sq_best = float(v0_sq[best_idx]) # Diagnostic lowest_idx = 0 @@ -2635,13 +2733,10 @@ def solve_linear_method( eigvals_lm[lowest_idx], v0_sq[lowest_idx], E_lm, - v0_sq[best_idx], + v0_sq_best, ) else: - logger.debug(" LM: selected eigenvalue E_LM = %.6f (|v0|^2 = %.4f)", E_lm, v0_sq[best_idx]) - - if v0_sq[best_idx] < 0.01: - logger.warning(" LM: max |v0|^2 = %.4f is small; update may be unreliable.", v0_sq[best_idx]) + logger.debug(" LM: selected eigenvalue E_LM = %.6f (|v0|^2 = %.4f)", E_lm, v0_sq_best) w = eigvecs_lm[:, best_idx] w0 = w[0] @@ -2655,12 +2750,12 @@ def solve_linear_method( logger.info( " LM: E_LM = %.6f (|v0|^2 = %.4f), ||c|| = %.3e, max|c| = %.3e", E_lm, - v0_sq[best_idx], + v0_sq_best, np.linalg.norm(c_vec), np.max(np.abs(c_vec)), ) - return c_vec, E_lm + return c_vec, E_lm, v0_sq_best @staticmethod def _shard_X_F(X_local: npt.NDArray, F_local: npt.NDArray): @@ -2776,6 +2871,9 @@ def _sr_solve_tall_cg_device( ``x0`` and ``y`` live in the sample space, shape ``(N_total,)``; ``y`` is the CG solution (suitable as warm-start next iteration). ``theta = X y`` lives in parameter space, shape ``(P,)``. + + Uses ``psum`` collectives (not ``all_gather(X)``) so per-rank peak + memory does not scale with ``mpi_size``. """ solver = _get_sr_tall_cg_kernel() X_g, F_g = self._shard_X_F(X_local, F_local) @@ -3711,7 +3809,7 @@ def apply_S_primal_numpy(v): else: logger.info( "Using conjugate gradient for the inverse of S " - "(device-resident, shard_map + all_gather, push-through identity)." + "(device-resident, shard_map + psum, push-through identity)." ) logger.info(f" [CG] threshold {sr_cg_tol}.") logger.info(f" [CG] max iteration: {sr_cg_max_iter}.") @@ -4065,13 +4163,26 @@ def apply_dual_S_numpy(v): ) # Solve LM eigenvalue problem - c_vec, E_lm = self.solve_linear_method(H_0_lm, f_vec_lm, S_mat, K_mat, B_mat, epsilon=lm_cond) - - if E_lm > H_0_lm + 3.0 * E_std: - logger.warning( - f"LM: E_LM={E_lm:.6f} > E_0 + 3*sigma = {H_0_lm:.6f} + 3*{E_std:.6f} = {H_0_lm + 3.0 * E_std:.6f}; " - f"LM does not predict improvement. Falling back to plain SR." - ) + c_vec, E_lm, v0_sq_best = self.solve_linear_method(H_0_lm, f_vec_lm, S_mat, K_mat, B_mat, epsilon=lm_cond) + + # Safety thresholds: + # (i) E_LM exceeds current energy by > 3 sigma -- LM does not + # predict an improvement; the linear model is unreliable. + # (ii) |v_0|^2 of the selected eigenvector is small -- the LM + # update sits far outside the linear regime, where the + # first-order extrapolation can produce wild parameter + # moves and downstream NaN/Inf energies. + # In either case fall back to a conservative plain-SR step. + _V0_SQ_MIN = 0.9 + _e_lm_bad = E_lm > H_0_lm + 3.0 * E_std + _v0_bad = v0_sq_best < _V0_SQ_MIN + if _e_lm_bad or _v0_bad: + reasons = [] + if _e_lm_bad: + reasons.append(f"E_LM={E_lm:.6f} > E_0+3*sigma={H_0_lm:.6f}+3*{E_std:.6f}={H_0_lm + 3.0 * E_std:.6f}") + if _v0_bad: + reasons.append(f"|v0|^2={v0_sq_best:.4f} < {_V0_SQ_MIN} (LM update outside linear regime)") + logger.warning("LM: " + "; ".join(reasons) + ". Falling back to plain SR.") theta = 0.1 * g_sr else: # Back-transform: c_vec[0] = c_0 (SR direction), c_vec[1:] = c_k (individual params) @@ -6702,7 +6813,7 @@ def solve_linear_method( K_matrix: npt.NDArray, B_matrix: npt.NDArray, epsilon: float, - ) -> tuple[npt.NDArray, float]: + ) -> tuple[npt.NDArray, float, float]: r"""Debug implementation of the Linear Method with dgelscut preconditioning. This mirrors ``MCMC.solve_linear_method`` using the same dgelscut @@ -6718,7 +6829,8 @@ def solve_linear_method( epsilon: dgelscut threshold (correlation matrix min eigenvalue). Returns: - (c_vec, E_lm): parameter update in original space and selected eigenvalue. + (c_vec, E_lm, v0_sq_best): parameter update in original space, + selected eigenvalue, and ``|v_0|^2`` of the selected eigenvector. """ # Delegate to MCMC.solve_linear_method -- the production version uses # the same dgelscut + S-orthonormalization + standard eigenvalue problem. diff --git a/jqmc_workflow/_cli.py b/jqmc_workflow/_cli.py index 2bba4af3..02a71a9e 100644 --- a/jqmc_workflow/_cli.py +++ b/jqmc_workflow/_cli.py @@ -44,13 +44,13 @@ import os import shutil -from datetime import datetime from logging import Formatter, StreamHandler, getLogger import toml import typer from ._config import get_config_dir, template_dir +from ._state import WorkflowStatus, get_jobs, update_job, update_status logger = getLogger("jqmc-workflow").getChild(__name__) @@ -74,14 +74,25 @@ def __init__(self, root_dir: str): self.root_dir = root_dir self.job_counter = 0 self.entries = [] # list of dicts with path, state info + self._visited: set[str] = set() def discover(self): """Walk tree and collect entries from workflow_state.toml.""" self.entries = [] self.job_counter = 0 + self._visited.clear() self._walk(self.root_dir) def _walk(self, path): + # Guard against symlink cycles: dedupe by realpath. + try: + key = os.path.realpath(path) + except OSError: + return + if key in self._visited: + return + self._visited.add(key) + state_file = os.path.join(path, "workflow_state.toml") if os.path.isfile(state_file): try: @@ -112,9 +123,12 @@ def _walk(self, path): except Exception as e: logger.warning(f"Failed to read {state_file}: {e}") - # Recurse into subdirs + # Recurse into subdirs. Pilot subdirectories (``_pilot*``) hold + # internal bookkeeping state for the parent workflow and should + # not be listed as separate user-facing jobs. This matches the + # exclusion in :func:`_state.get_all_workflow_statuses`. try: - subdirs = sorted(d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d))) + subdirs = sorted(d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d)) and not d.startswith("_pilot")) except PermissionError: return for d in subdirs: @@ -319,14 +333,16 @@ def delete_job(self, job_id: int, server_machine_name: str): except Exception as ex: logger.error(f" Failed to delete job on {server}: {ex}") - # Update workflow_state.toml - state_file = os.path.join(e["dir"], "workflow_state.toml") - if os.path.isfile(state_file): - data = toml.load(state_file) - data.setdefault("workflow", {})["status"] = "cancelled" - data["workflow"]["updated_at"] = datetime.now().isoformat() - with open(state_file, "w") as f: - toml.dump(data, f) + # Update workflow_state.toml via the canonical API so that + # [[jobs]] (latest record) and [workflow] stay consistent. + if os.path.isfile(os.path.join(e["dir"], "workflow_state.toml")): + jobs = get_jobs(e["dir"]) + if jobs: + last_job = jobs[-1] + input_file = last_job.get("input_file") + if input_file and last_job.get("status") in ("submitted", "completed"): + update_job(e["dir"], input_file, status="cancelled") + update_status(e["dir"], WorkflowStatus.CANCELLED) logger.info(f" Status set to 'cancelled' for JOB-ID {job_id}.") diff --git a/jqmc_workflow/_error_estimator.py b/jqmc_workflow/_error_estimator.py index 4b9cdbb9..b002d94c 100644 --- a/jqmc_workflow/_error_estimator.py +++ b/jqmc_workflow/_error_estimator.py @@ -164,6 +164,50 @@ def estimate_additional_steps( return additional +def read_accumulated_measurement_steps( + restart_chk_path: str, + warmup: int, + collect_steps: int = 0, +) -> int | None: + """Read the actual accumulated measurement steps from a jQMC checkpoint. + + Returns ``raw_mcmc_counter - collect_steps - warmup``, i.e. the number + of binnable measurement samples reflected in the checkpoint's + observable arrays (matching :pymeth:`MCMC.get_E` / + :pymeth:`GFMC_t.get_E` post-processing logic, where the public + ``mcmc_counter`` property already subtracts ``collect_steps``). + + This is the source of truth for the accumulated sample count when a + run was interrupted by ``max_time`` and only partially completed its + planned ``num_mcmc_steps`` -- in that case the planned step count + over-estimates the actual samples on disk. + + Args: + restart_chk_path: Path to ``restart.h5`` (merged checkpoint). + warmup: ``num_(gfmc_)mcmc_warmup_steps`` from the workflow. + collect_steps: ``num_gfmc_collect_steps`` for LRDMC, 0 for MCMC. + + Returns: + Effective accumulated measurement-step count (>= 0), or *None* + if the checkpoint cannot be read. + """ + try: + from jqmc._checkpoint import load_driver_config_from_checkpoint + except ImportError as exc: + logger.warning(f"jqmc not importable; cannot read mcmc_counter from {restart_chk_path}: {exc}") + return None + try: + cfg = load_driver_config_from_checkpoint(restart_chk_path, rank=0) + except Exception as exc: + logger.warning(f"Cannot read driver_config from {restart_chk_path}: {exc}") + return None + if "mcmc_counter" not in cfg: + logger.warning(f"driver_config in {restart_chk_path} has no 'mcmc_counter' key.") + return None + raw = int(cfg["mcmc_counter"]) + return max(raw - int(collect_steps) - int(warmup), 0) + + def suffixed_name(filename: str, index: int) -> str: """Insert an integer suffix before the file extension. diff --git a/jqmc_workflow/_input_generator.py b/jqmc_workflow/_input_generator.py index 5b1ee3f5..d202411c 100644 --- a/jqmc_workflow/_input_generator.py +++ b/jqmc_workflow/_input_generator.py @@ -37,6 +37,7 @@ # POSSIBILITY OF SUCH DAMAGE. import copy +import os from logging import getLogger import toml @@ -152,13 +153,21 @@ def generate_input_toml( f"Required parameter '{k}' in [{section}] was not set. Please provide it via the 'overrides' dict." ) + # Atomic write: tmpfile + fsync + os.replace. A partial input TOML + # would otherwise block the next workflow run with a parse error. + tmp = filename + ".tmp" if with_comments: text = _dump_with_comments(params, job_type) - with open(filename, "w") as f: + with open(tmp, "w") as f: f.write(text) + f.flush() + os.fsync(f.fileno()) else: - with open(filename, "w") as f: + with open(tmp, "w") as f: toml.dump(params, f) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp, filename) logger.info(f"Generated {filename} (job_type={job_type})") return filename @@ -195,7 +204,11 @@ def _toml_value(v) -> str: if isinstance(v, bool): return "true" if v else "false" if isinstance(v, str): - return f'"{v}"' + # JSON-compatible escaping matches TOML basic-string rules + # (backslash, quotes, control characters). + import json + + return json.dumps(v, ensure_ascii=False) if isinstance(v, (int, float)): return str(v) if isinstance(v, dict): diff --git a/jqmc_workflow/_job.py b/jqmc_workflow/_job.py index 1acc1116..01f1dc11 100644 --- a/jqmc_workflow/_job.py +++ b/jqmc_workflow/_job.py @@ -102,7 +102,6 @@ def __init__( queue_label: str = "default", jobname: str = "jqmc-wf", run_id: str = "", - safe_mode: bool = False, ): self.data_transfer = Data_transfer( server_machine_name=server_machine_name, @@ -142,7 +141,6 @@ def __init__( self.run_id = run_id self.input_file = input_file self.output_file = output_file - self.safe_mode = safe_mode # -- Job state --------------------------------------------- self.max_job_submit = self.queue_data.get("max_job_submit", 1000) @@ -410,5 +408,8 @@ def job_acct(self) -> tuple[str, str, str] | None: # -- Helper ---------------------------------------------------- def _close_ssh(self): - self.server_machine.ssh_close() + # data_transfer owns the same Machine instance and recursively + # calls ssh_close() on it via Machines_handler; one call suffices. + # ssh_close is idempotent (no-op after the first call), so a + # second close here would just be wasted work. self.data_transfer.ssh_close() diff --git a/jqmc_workflow/_machine.py b/jqmc_workflow/_machine.py index a532bea6..266cdd66 100644 --- a/jqmc_workflow/_machine.py +++ b/jqmc_workflow/_machine.py @@ -36,10 +36,12 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import concurrent.futures import os import pathlib import random import re +import shlex import shutil import stat import subprocess @@ -151,6 +153,13 @@ def ssh_open(self): logger.debug(f"SSH already open (id={id(self.ssh)})") return + # Note: this is a *synchronous* sleep, even when ``ssh_open`` is + # reached from an asyncio coroutine (e.g. ``_submit_and_wait``). + # It blocks the event loop for 3-6 s. Parallel workflows + # (Launcher / LRDMC_Ext) effectively serialise here. Replacing + # this whole layer with asyncssh + ``await asyncio.sleep`` would + # restore parallelism; until then, prefer fewer SSH opens per + # workflow rather than tighter polling intervals. rw = random.randint(3, 6) logger.info(f" Wait {rw}s before opening SSH to {self.name}") time.sleep(rw) @@ -215,11 +224,13 @@ def ssh_open(self): except paramiko.ssh_exception.SSHException: # Clean up the ProxyCommand from this failed attempt self._kill_proxy_process(proxy_cmd) - logger.warning(f"SSH connect failed (attempt {tt + 1}). Retrying in {self.ssh_retry_time}s.") - time.sleep(self.ssh_retry_time) if tt == self.ssh_retry_max_num - 1: + # Re-raise immediately on the final attempt; no point + # sleeping ssh_retry_time only to give up afterwards. logger.error("SSH connect failed after all retries.") raise + logger.warning(f"SSH connect failed (attempt {tt + 1}). Retrying in {self.ssh_retry_time}s.") + time.sleep(self.ssh_retry_time) self.sftp = self.ssh.open_sftp() self.ssh_status = True @@ -344,7 +355,7 @@ def run_command(self, command: str, execute_dir: str = None): return self._run_local(command_r) return self._run_remote(command_r) - def _run_local(self, command_r: str, max_retries: int = 10): + def _run_local(self, command_r: str, max_retries: int = 3): for attempt in range(max_retries): for sub_attempt in range(3): try: @@ -368,32 +379,64 @@ def _run_local(self, command_r: str, max_retries: int = 10): logger.warning(f"Local command timeout (sub-attempt {sub_attempt})") time.sleep(60) - logger.warning(f"Local command failed (attempt {attempt}). Retrying in {self.ssh_retry_time}s.") - time.sleep(self.ssh_retry_time) + if attempt < max_retries - 1: + logger.warning( + f"Local command failed (attempt {attempt + 1}/{max_retries}). Retrying in {self.ssh_retry_time}s." + ) + time.sleep(self.ssh_retry_time) raise RuntimeError(f"Local command failed after {max_retries} retries: {command_r}") + remote_command_timeout_sec = 1200 # match _run_local default + def _run_remote(self, command_r: str): + """Execute *command_r* on the remote host with a hard wall-time guard. + + ``recv_exit_status`` waits on a paramiko ``status_event`` that is + *not* connected to socket-level keepalive, so a dead SSH session + can hang the call indefinitely. We run the entire exec/read + sequence in a worker thread and enforce a timeout via + :class:`concurrent.futures.Future`; on timeout the SSH session is + torn down so the next call reconnects cleanly. + """ self.ssh_open() + + def _do_exec(): + try: + pstdin, pstdout, pstderr = self.ssh.exec_command(command=command_r) + except (paramiko.SSHException, OSError, EOFError): + # Connection may have died (e.g. keepalive timeout during + # a long asyncio.sleep between polls). Reconnect once. + logger.warning("SSH connection lost during exec_command; reconnecting...") + self.ssh_close() + self.ssh_open() + pstdin, pstdout, pstderr = self.ssh.exec_command(command=command_r) + try: + exit_status = pstdout.channel.recv_exit_status() + stdout = pstdout.read().decode("utf-8").strip() + stderr = pstderr.read().decode("utf-8").strip() + finally: + for ch in (pstdin, pstdout, pstderr): + try: + ch.close() + except Exception: + pass + return exit_status, stdout, stderr + + executor = ThreadPoolExecutor(max_workers=1) + future = executor.submit(_do_exec) try: - pstdin, pstdout, pstderr = self.ssh.exec_command(command=command_r) - except (paramiko.SSHException, OSError, EOFError): - # Connection may have died (e.g. keepalive timeout during - # a long asyncio.sleep between polls). Reconnect once. - logger.warning("SSH connection lost during exec_command; reconnecting...") - self.ssh_close() - self.ssh_open() - pstdin, pstdout, pstderr = self.ssh.exec_command(command=command_r) - try: - exit_status = pstdout.channel.recv_exit_status() - stdout = pstdout.read().decode("utf-8").strip() - stderr = pstderr.read().decode("utf-8").strip() + exit_status, stdout, stderr = future.result(timeout=self.remote_command_timeout_sec) + except concurrent.futures.TimeoutError as exc: + logger.error(f"Remote command timed out after {self.remote_command_timeout_sec}s: {command_r}") + try: + self.ssh_close() + except Exception: + pass + raise RuntimeError(f"Remote command timed out after {self.remote_command_timeout_sec}s: {command_r}") from exc finally: - for ch in (pstdin, pstdout, pstderr): - try: - ch.close() - except Exception: - pass + executor.shutdown(wait=False) + if exit_status != 0: logger.error(f"Remote command failed: {command_r}") logger.error(f"stdout={stdout}") @@ -423,13 +466,19 @@ def _sftp_lstat_with_retry(self, path: str, max_retries=3, timeout_sec=5.0): def is_file(self, file_name: str) -> bool: if self.machine_type == "local": return os.path.isfile(file_name) - fileattr = self._sftp_lstat_with_retry(file_name) + try: + fileattr = self._sftp_lstat_with_retry(file_name) + except (RuntimeError, OSError): + return False return stat.S_ISREG(fileattr.st_mode) def is_dir(self, dir_name: str) -> bool: if self.machine_type == "local": return os.path.isdir(dir_name) - fileattr = self._sftp_lstat_with_retry(dir_name) + try: + fileattr = self._sftp_lstat_with_retry(dir_name) + except (RuntimeError, OSError): + return False return stat.S_ISDIR(fileattr.st_mode) def exist(self, object_name: str) -> bool: @@ -451,7 +500,7 @@ def get_job_list_as_text(self): return stdout.split("\n") def delete_job(self, jobid): - stdout, _ = self.run_command(f"{self.jobdel} {jobid}") + stdout, _ = self.run_command(f"{self.jobdel} {shlex.quote(str(jobid))}") return stdout.split("\n") @@ -493,7 +542,7 @@ def _get_sftp_file(self, source, target, exclude_patterns): def _put_sftp_file(self, source, target, exclude_patterns): if exclude_patterns and any(re.match(p, os.path.basename(source)) for p in exclude_patterns): return - self.server_machine.run_command(f"mkdir -p {os.path.dirname(target)}") + self.server_machine.run_command(f"mkdir -p {shlex.quote(os.path.dirname(target))}") self.server_machine.ssh_open() self.server_machine.sftp.put(source, target) @@ -513,7 +562,7 @@ def _get_sftp_dir(self, source, target, exclude_patterns): self._get_sftp_dir(remote_path, local_path, exclude_patterns) def _put_sftp_dir(self, source, target, exclude_patterns): - self.server_machine.run_command(f"mkdir -p {target}") + self.server_machine.run_command(f"mkdir -p {shlex.quote(target)}") self.server_machine.ssh_open() sftp = self.server_machine.sftp for item in os.listdir(source): @@ -539,7 +588,7 @@ def _transfer(self, from_path, to_path, exclude_patterns, dir_transfer, directio # Ensure target directory exists to_dir = os.path.dirname(to_path) if not dir_transfer else to_path if direction == "put": - self.server_machine.run_command(f"mkdir -p {to_dir}") + self.server_machine.run_command(f"mkdir -p {shlex.quote(to_dir)}") else: os.makedirs(to_dir, exist_ok=True) diff --git a/jqmc_workflow/_output_parser.py b/jqmc_workflow/_output_parser.py index 48394b32..735e4aec 100644 --- a/jqmc_workflow/_output_parser.py +++ b/jqmc_workflow/_output_parser.py @@ -211,11 +211,12 @@ def repair_forces_from_output(work_dir: str) -> bool: if forces is None: return False - # Update the TOML - state = toml.load(state_path) + # Update the TOML atomically via the canonical state API. + from ._state import _write, read_state + + state = read_state(work_dir) state.setdefault("result", {})["forces"] = forces - with open(state_path, "w") as f: - toml.dump(state, f) + _write(work_dir, state) logger.info(f" Repaired forces in {work_dir} from {os.path.basename(last_out)}") return True @@ -228,24 +229,27 @@ def repair_forces_from_output(work_dir: str) -> bool: # "Optimization step = 1/10" or "Optimization step = 1/10." _RE_OPT_STEP = re.compile(r"Optimization\s+step\s*=\s*(\d+)\s*/\s*(\d+)") +# Numeric pattern that also matches ``nan`` / ``inf`` (any case). +# Required so a diverged QMC run surfaces as a non-finite float rather +# than being silently dropped from per-step parsing. +_NUM = r"[+-]?(?:\d+\.?\d*(?:[eE][+-]?\d+)?|nan|inf)" + # "E = -76.438901 +- 0.000123 Ha" (energy line) _RE_ENERGY = re.compile( - r"E\s*=\s*([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?)" - r"\s*\+\-\s*" - r"(\d+\.?\d*(?:[eE][+-]?\d+)?)" + rf"E\s*=\s*({_NUM})\s*\+\-\s*({_NUM})", + re.IGNORECASE, ) # "Max f = 17.984 +- 0.330 Ha/a.u." or "Max f = 17.984 +- 0.330" _RE_MAX_FORCE = re.compile( - r"Max\s+f\s*=\s*([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?)" - r"\s*\+\-\s*" - r"(\d+\.?\d*(?:[eE][+-]?\d+)?)" + rf"Max\s+f\s*=\s*({_NUM})\s*\+\-\s*({_NUM})", + re.IGNORECASE, ) # "Max of signal-to-noise of f = max(|f|/|std f|) = 126.871." _RE_SNR = re.compile( - r"Max of signal-to-noise of f\s*=\s*max\(\|f\|/\|std f\|\)\s*=\s*" - r"([-+]?\d+(?:\.\d+)?)" + rf"Max of signal-to-noise of f\s*=\s*max\(\|f\|/\|std f\|\)\s*=\s*({_NUM})", + re.IGNORECASE, ) # "Average of walker weights is 0.799. Ideal is ~ 0.800. Adjust epsilon_AS." @@ -382,6 +386,11 @@ def _find_input_files(work_dir: str) -> list: Reads ``workflow_state.toml`` ``[[jobs]]`` records and returns the ``input_file`` paths that exist on disk, ordered by ``step``. + + Only jobs in ``"fetched"`` or ``"completed"`` status are considered: + cancelled / failed / still-submitted jobs may have stale inputs + that would mislead downstream parsing (e.g. the wrong hamiltonian + file reference). """ state_path = os.path.join(work_dir, "workflow_state.toml") if not os.path.isfile(state_path): @@ -392,6 +401,8 @@ def _find_input_files(work_dir: str) -> list: return [] files = [] for job in state.get("jobs", []): + if job.get("status") not in ("fetched", "completed"): + continue name = job.get("input_file", "") if name: path = os.path.join(work_dir, name) @@ -496,6 +507,11 @@ def _find_output_files(work_dir: str) -> list: Reads ``workflow_state.toml`` ``[[jobs]]`` records and returns the ``output_file`` paths that exist on disk, ordered by ``step``. + + Only jobs in ``"fetched"`` or ``"completed"`` status are considered: + partial output left behind by cancelled / failed / still-submitted + jobs would otherwise pollute parser aggregations (timing breakdowns, + SNR series, force tables) with garbage data. """ state_path = os.path.join(work_dir, "workflow_state.toml") if not os.path.isfile(state_path): @@ -506,6 +522,8 @@ def _find_output_files(work_dir: str) -> list: return [] files = [] for job in state.get("jobs", []): + if job.get("status") not in ("fetched", "completed"): + continue name = job.get("output_file", "") if name: path = os.path.join(work_dir, name) diff --git a/jqmc_workflow/_phase.py b/jqmc_workflow/_phase.py index 312d20a9..55d35e61 100644 --- a/jqmc_workflow/_phase.py +++ b/jqmc_workflow/_phase.py @@ -221,12 +221,12 @@ def allowed_actions( ) -> list[str]: """Return the list of actions allowed for the given *phase* / *status*. - When *status* is ``FAILED`` only ``recover_*`` and ``rollback_phase`` - actions are kept. When *status* is ``RUNNING`` configuration actions - are excluded. + When *status* is ``FAILED`` or ``CANCELLED`` only ``recover_*`` and + ``rollback_phase`` actions are kept. When *status* is ``RUNNING`` + configuration actions are excluded. """ phase_actions = list(PHASE_ALLOWED_ACTIONS.get(phase, [])) - if status == WorkflowStatus.FAILED: + if status in (WorkflowStatus.FAILED, WorkflowStatus.CANCELLED): phase_actions = [a for a in phase_actions if a.startswith("recover_")] phase_actions.append("rollback_phase") if status == WorkflowStatus.RUNNING: diff --git a/jqmc_workflow/_state.py b/jqmc_workflow/_state.py index aa4442a5..8ba84993 100644 --- a/jqmc_workflow/_state.py +++ b/jqmc_workflow/_state.py @@ -17,6 +17,7 @@ completed -- scheduler reports job finished fetched -- results transferred back to local machine failed -- job failed + cancelled -- user cancelled via CLI before completion """ # Copyright (C) 2024- Kosuke Nakano @@ -80,6 +81,7 @@ class JobStatus(str, Enum): COMPLETED = "completed" FETCHED = "fetched" FAILED = "failed" + CANCELLED = "cancelled" class CompletionStatus(str, Enum): @@ -107,7 +109,9 @@ class CompletionStatus(str, Enum): def _now_iso() -> str: - return datetime.now().isoformat(timespec="seconds") + # Local time *with* tz suffix: stays unambiguous when state files are + # shared between machines in different zones or compared across DST. + return datetime.now().astimezone().isoformat(timespec="seconds") def create_state( @@ -118,9 +122,9 @@ def create_state( ) -> dict: """Create (or reset) workflow_state.toml in *directory*. - If the file already exists, the ``[estimation]`` and ``[[jobs]]`` - sections are preserved so that pilot-run results and job history - survive a restart. + If the file already exists, ``[estimation]``, ``[[jobs]]``, and + ``[input_fingerprints]`` are preserved so that pilot-run results, + job history, and the staleness baseline survive a restart. """ if status not in VALID_STATUSES: raise ValueError(f"Invalid status '{status}'. Must be one of {VALID_STATUSES}") @@ -129,6 +133,7 @@ def create_state( existing = read_state(directory) preserved_estimation = existing.get("estimation", {}) preserved_jobs = existing.get("jobs", []) + preserved_fingerprints = existing.get("input_fingerprints", {}) state = { "workflow": { @@ -144,6 +149,8 @@ def create_state( if preserved_estimation: state["estimation"] = preserved_estimation + if preserved_fingerprints: + state["input_fingerprints"] = preserved_fingerprints _write(directory, state) return state @@ -155,49 +162,163 @@ def read_state(directory: str) -> dict: if not os.path.isfile(path): return {} state = toml.load(path) - # Migrate legacy single [job] -> [[jobs]] list + # Migrate legacy single [job] -> [[jobs]] list. Persist the + # migration so subsequent reads don't repeat the conversion. if "job" in state and "jobs" not in state: old_job = state.pop("job") - if old_job: - state["jobs"] = [old_job] - else: - state["jobs"] = [] + state["jobs"] = [old_job] if old_job else [] + try: + _write(directory, state) + except OSError as exc: + logger.warning(f"Failed to persist legacy [job] migration in {path}: {exc}") return state -def _check_normal_termination(directory: str, jobs: list) -> list[str]: - """Check fetched output files for the ``Program ends`` marker. +def _has_program_ends(filepath: str) -> bool | None: + """Return ``True`` if *filepath*'s tail contains ``Program ends``. + + ``None`` if the file is absent or unreadable (caller decides how to + treat that -- :func:`_check_normal_termination` ignores absent files + as "nothing to assert", while :func:`reconcile_fetched_jobs` treats + them as "not yet finished"). Only the last 8 KiB is read since the + marker is always the final log line. + """ + if not os.path.isfile(filepath): + return None + try: + with open(filepath, errors="replace") as f: + f.seek(0, 2) + size = f.tell() + f.seek(max(0, size - 8192)) + tail = f.read() + except OSError: + return None + return "Program ends" in tail - Returns a list of output-file names that exist on disk but do **not** - contain the ``Program ends`` line -- a strong signal that the - computation was killed (e.g. wall-time expiration) before normal - termination. - Files that are absent, unreadable, or binary are silently skipped. +def _check_normal_termination(directory: str, jobs: list) -> list[str]: + """Return output-file names whose contents lack ``Program ends``. + + Only jobs that were *meant* to complete normally are inspected -- + namely status ``"fetched"`` or ``"completed"``. Jobs in + ``"submitted"``, ``"failed"``, or ``"cancelled"`` status are skipped + because their output is expected to be incomplete (still running, + already known-failed, or intentionally aborted), and a partial file + left over from such a job would otherwise produce a false-positive + "abnormal termination" verdict that flips the whole workflow to + FAILED. + + Files that are absent on disk are silently skipped -- they say + nothing about whether the remote computation ended normally. Files + present without the marker are reported as abnormal terminations + (e.g. wall-time kill, process crash). """ abnormal: list[str] = [] for job in jobs: + if job.get("status") not in ("fetched", "completed"): + continue output_file = job.get("output_file", "") if not output_file: continue - filepath = os.path.join(directory, output_file) - if not os.path.isfile(filepath): - continue # not fetched yet -- nothing to check - try: - with open(filepath, errors="replace") as f: - # Read only the tail (last 8 KiB) for efficiency; - # "Program ends ..." is always the last log line. - f.seek(0, 2) - size = f.tell() - f.seek(max(0, size - 8192)) - tail = f.read() - if "Program ends" not in tail: - abnormal.append(output_file) - except OSError: - continue # unreadable -- skip + result = _has_program_ends(os.path.join(directory, output_file)) + if result is False: + abnormal.append(output_file) return abnormal +def reconcile_fetched_jobs_recursive(directory: str) -> int: + """Run :func:`reconcile_fetched_jobs` on *directory* and every nested + sub-directory that contains its own ``workflow_state.toml``. + + LRDMC / MCMC / VMC pilots live in subdirectories (``_pilot_b/``, + ``_pilot_a/_pilot1/``, ``_pilot/``, ...) and each carries its own + state file. Calling :func:`reconcile_fetched_jobs` only on the + top-level workflow directory misses those -- so a pilot job that + finished on the cluster but whose state record is stuck on + ``"submitted"`` (e.g. orchestrator died between job completion and + fetch-finalize) would not be picked up before the production phase + tries to resume it via SSH. + + A malformed state.toml in any nested directory is logged and + skipped; the walk continues so a single corrupted pilot record + does not block production-level reconciliation. + + Returns the total number of jobs reconciled across all directories. + """ + total = 0 + for dirpath, dirnames, filenames in os.walk(directory): + if STATE_FILENAME not in filenames: + continue + try: + total += reconcile_fetched_jobs(dirpath) + except Exception as exc: + logger.warning( + f"reconcile_fetched_jobs_recursive: skipping {dirpath} due to error ({exc.__class__.__name__}: {exc})" + ) + return total + + +def reconcile_fetched_jobs(directory: str) -> int: + """Promote orphaned ``[[jobs]]`` records to ``"fetched"``. + + A job whose status is ``"submitted"`` or ``"completed"`` is promoted + to ``"fetched"`` when **both**: + + * its ``output_file`` is present locally with a ``Program ends`` + marker (the run finished normally on remote), AND + * at least one ``.h5`` checkpoint is present in the directory + (the workflow has a restart point to continue from). + + The ``.h5`` precondition prevents the next phase from crashing with + ``"no restart checkpoint found"`` when only the ``.out`` got + rsync'd locally but ``restart.h5`` is still on the remote. In + that case we leave the job ``"submitted"`` so the normal + ``_submit_and_wait`` resume path re-fetches via SSH. + + Handles the case where the workflow process was killed between + job completion and the fetch-finalize state update, while the + actual output and restart files have since landed locally + (e.g. via rsync or a separate fetch). + + Returns the number of jobs reconciled. + """ + state = read_state(directory) + jobs = state.get("jobs", []) + if not jobs or not os.path.isdir(directory): + return 0 + # Cheap one-shot listdir: avoids per-job globbing. Promotion is + # only safe if *some* checkpoint is on disk; we don't require an + # exact name match because workflows accept several conventions + # (``restart.h5``, ``lrdmc.h5``, ``hamiltonian_data_opt_step_*.h5``). + has_h5 = any(fn.endswith(".h5") for fn in os.listdir(directory)) + reconciled = 0 + for job in jobs: + status = job.get("status") + if status not in ("submitted", "completed"): + continue + output_file = job.get("output_file", "") + if not output_file: + continue + if _has_program_ends(os.path.join(directory, output_file)) is not True: + continue + if not has_h5: + logger.warning( + f"reconcile_fetched_jobs: not promoting {output_file} -- " + f"no .h5 checkpoint in {directory}; will let normal resume " + f"path try to fetch the missing checkpoint via SSH." + ) + continue + now = _now_iso() + job["status"] = "fetched" + job.setdefault("completed_at", now) + job["fetched_at"] = now + reconciled += 1 + if reconciled: + state.setdefault("workflow", {})["updated_at"] = _now_iso() + _write(directory, state) + return reconciled + + def validate_completion( directory: str, output_values: dict | None = None, @@ -320,9 +441,14 @@ def update_status( logger.warning(f"No workflow_state.toml in {directory}; creating minimal one.") state = {"workflow": {}, "jobs": [], "result": {}} - state.setdefault("workflow", {}) - state["workflow"]["status"] = status_str - state["workflow"]["updated_at"] = _now_iso() + wf = state.setdefault("workflow", {}) + # Populate required identity fields if missing (e.g. when called + # without a prior Container.create_state). + wf.setdefault("label", os.path.basename(os.path.abspath(directory)) or "workflow") + wf.setdefault("type", "Workflow") + wf.setdefault("created_at", _now_iso()) + wf["status"] = status_str + wf["updated_at"] = _now_iso() if phase is not None: state["workflow"]["phase"] = phase.value if hasattr(phase, "value") else phase @@ -664,8 +790,19 @@ def get_input_fingerprints(directory: str) -> dict[str, dict]: def _write(directory: str, state: dict): - """Write state dict to workflow_state.toml.""" + """Write state dict to workflow_state.toml atomically. + + Writes to a sibling ``.tmp`` file, ``fsync``s it, then ``os.replace``s + over the destination. This guarantees that ``workflow_state.toml`` + either reflects the previous successful write or the new state in + full -- never a truncated mix -- even if the process is killed + mid-write (SIGKILL, power loss, ...). + """ path = os.path.join(directory, STATE_FILENAME) os.makedirs(directory, exist_ok=True) - with open(path, "w") as f: + tmp = path + ".tmp" + with open(tmp, "w") as f: toml.dump(state, f) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp, path) diff --git a/jqmc_workflow/_transfer.py b/jqmc_workflow/_transfer.py index 5f718d27..5d88c057 100644 --- a/jqmc_workflow/_transfer.py +++ b/jqmc_workflow/_transfer.py @@ -38,6 +38,7 @@ import fnmatch import glob import os +import shlex from logging import getLogger from ._machine import Machine, Machines_handler @@ -239,7 +240,13 @@ def get_objects(self, from_objects=None, exclude_patterns=None, *, work_dir=None # -- remove (local + remote) ---------------------------------- - def remove_objects(self, patterns: list[str], *, work_dir: str | None = None) -> None: + def remove_objects( + self, + patterns: list[str], + *, + work_dir: str | None = None, + protected_basenames: "frozenset[str] | set[str] | None" = None, + ) -> None: """Delete files matching *patterns* from local and (if remote) server. Matching is **recursive** -- each pattern is applied to *work_dir* @@ -252,16 +259,26 @@ def remove_objects(self, patterns: list[str], *, work_dir: str | None = None) -> the top-level directory **and** all subdirectories. work_dir (str, optional): Local directory. When *None*, falls back to ``os.getcwd()``. + protected_basenames (frozenset[str], optional): + Basenames that must never be deleted, even when matched by + a pattern. Used by :meth:`Workflow._cleanup_files` to + preserve ``workflow_state.toml`` against over-broad + patterns like ``"*.toml"``. """ local_cwd = os.path.abspath(work_dir) if work_dir else os.path.abspath(os.getcwd()) + protected = frozenset(protected_basenames or ()) # -- Local deletion (always, recursive) ------------------- for pattern in patterns: for fpath in sorted(glob.glob(os.path.join(local_cwd, "**", pattern), recursive=True)): - if os.path.isfile(fpath): - os.remove(fpath) - relpath = os.path.relpath(fpath, local_cwd) - logger.info(f" Cleanup: removed local file {relpath}") + if not os.path.isfile(fpath): + continue + if os.path.basename(fpath) in protected: + logger.warning(f" Cleanup: refusing to delete protected file {os.path.relpath(fpath, local_cwd)}") + continue + os.remove(fpath) + relpath = os.path.relpath(fpath, local_cwd) + logger.info(f" Cleanup: removed local file {relpath}") # -- Remote deletion (only for non-local machines) -------- if self.server_machine.machine_type == "local": @@ -278,9 +295,15 @@ def remove_objects(self, patterns: list[str], *, work_dir: str | None = None) -> return server_dir = local_cwd.replace(local_root, server_root) + # Build a `find ... -name X ! -name P1 ! -name P2 ... -delete` clause + # so protected files are spared remotely as well. + protect_clause = " ".join(f"! -name {shlex.quote(p)}" for p in sorted(protected)) for pattern in patterns: try: - self.server_machine.run_command(f"find {server_dir} -name '{pattern}' -type f -delete") + # Quote both the directory and the user-supplied glob to + # prevent shell-meta-character injection. + cmd = (f"find {shlex.quote(server_dir)} -name {shlex.quote(pattern)} {protect_clause} -type f -delete").rstrip() + self.server_machine.run_command(cmd) logger.info(f" Cleanup: removed remote files matching {pattern} (recursive)") except Exception as exc: logger.warning(f" Cleanup: failed to remove remote '{pattern}': {exc}") diff --git a/jqmc_workflow/launcher.py b/jqmc_workflow/launcher.py index 7fd94b5c..bea8c1ba 100644 --- a/jqmc_workflow/launcher.py +++ b/jqmc_workflow/launcher.py @@ -296,7 +296,7 @@ def get_session_state(self) -> dict: from ._state import get_workflow_summary workflows = {} - completed = failed = 0 + completed = failed = cancelled = 0 running_labels: list[str] = [] pending_labels: list[str] = [] @@ -311,6 +311,8 @@ def get_session_state(self) -> dict: completed += 1 elif s == "failed": failed += 1 + elif s == "cancelled": + cancelled += 1 elif s in ("running", "submitted"): running_labels.append(cw.label) else: @@ -322,6 +324,7 @@ def get_session_state(self) -> dict: "progress": { "completed": completed, "failed": failed, + "cancelled": cancelled, "running": running_labels, "pending": pending_labels, "total": len(self.workflows), @@ -417,7 +420,16 @@ async def async_launch(self): """ completed = set() failed = set() + skipped = set() # failed transitively because an upstream dep failed pending = set(self.workflows_by_label.keys()) + # label -> {"reason": str, "where": str, "kind": str} + failure_info: dict[str, dict] = {} + + def _where(label: str) -> str: + cw = self.workflows_by_label.get(label) + if cw is None: + return "(unknown)" + return getattr(cw, "project_dir", None) or getattr(cw, "dirname", None) or "(unknown)" logger.info("") logger.info("=" * 50) @@ -433,10 +445,17 @@ async def async_launch(self): for label in list(pending): deps = self.dependency_dict[label] # If any dep failed, this workflow cannot run - if any(d in failed for d in deps): - logger.error(f"[{label}] Skipping -- dependency failed: {[d for d in deps if d in failed]}") + failed_deps = [d for d in deps if d in failed] + if failed_deps: + logger.error(f"[{label}] Skipping -- dependency failed: {failed_deps}") pending.discard(label) failed.add(label) + skipped.add(label) + failure_info[label] = { + "kind": "skipped", + "reason": f"upstream dependency failed: {failed_deps}", + "where": _where(label), + } continue # All deps done? if all(d in completed for d in deps): @@ -450,12 +469,20 @@ async def async_launch(self): logger.info("-" * 50) logger.info(f" [{label}] Launching...") logger.info("-" * 50) - task = asyncio.create_task(self._run_workflow(label, cw)) + task = asyncio.create_task(self._run_workflow(label, cw), name=label) running[label] = task if not running: if pending: logger.error(f"Deadlock! Remaining: {pending}") + for label in pending: + failure_info[label] = { + "kind": "deadlock", + "reason": "deadlock: dependencies could not be resolved", + "where": _where(label), + } + failed.update(pending) + pending.clear() break break @@ -463,26 +490,50 @@ async def async_launch(self): done_tasks, _ = await asyncio.wait(running.values(), return_when=asyncio.FIRST_COMPLETED) for task in done_tasks: - # Find which label this task corresponds to - label = None - for lbl, t in list(running.items()): - if t is task: - label = lbl - break - if label is None: + label = task.get_name() + if label not in running: continue - del running[label] + # Task.exception() raises CancelledError on a cancelled + # task -- check cancellation BEFORE inspecting exception + # so the Launcher's main loop doesn't crash if a single + # workflow task is cancelled. + if task.cancelled(): + logger.warning(f"[{label}] CANCELLED") + failed.add(label) + failure_info[label] = { + "kind": "cancelled", + "reason": "task cancelled", + "where": _where(label), + } + continue + exc = task.exception() if exc: logger.error(f"[{label}] FAILED: {exc}") failed.add(label) + failure_info[label] = { + "kind": "exception", + "reason": f"{type(exc).__name__}: {exc}", + "where": _where(label), + } else: cw = self.workflows_by_label[label] if getattr(cw, "status", None) == "failed": - logger.error(f"[{label}] FAILED (status=failed)") + err = "" + try: + err = (cw.output_values or {}).get("error", "") + except Exception: + err = "" + msg = err or "workflow returned status=failed (no detail)" + logger.error(f"[{label}] FAILED (status=failed): {msg}") failed.add(label) + failure_info[label] = { + "kind": "status_failed", + "reason": msg, + "where": _where(label), + } else: logger.info(f"[{label}] Completed.") completed.add(label) @@ -493,8 +544,25 @@ async def async_launch(self): logger.info(" DAG execution summary") logger.info("-" * 50) logger.info(f" Completed : {len(completed)}") - logger.info(f" Failed : {len(failed)}") - logger.info(f" Skipped : {len(pending)}") + logger.info(f" Failed : {len(failed)} (of which skipped due to upstream: {len(skipped)})") + logger.info(f" Pending : {len(pending)}") + if failure_info: + logger.info("-" * 50) + logger.info(" Failure details") + logger.info("-" * 50) + # Show direct failures first, then transitively-skipped ones, + # so the root cause is easy to spot at the top. + order = {"exception": 0, "status_failed": 1, "cancelled": 2, "deadlock": 3, "skipped": 4} + for label in sorted(failure_info, key=lambda lb: (order.get(failure_info[lb]["kind"], 99), lb)): + info = failure_info[label] + logger.info(f" - [{label}] ({info['kind']})") + logger.info(f" where : {info['where']}") + # Truncate very long reason strings so the summary stays readable; + # the full message is already in the body of the log above. + reason = info["reason"] + if len(reason) > 400: + reason = reason[:400] + " ...[truncated]" + logger.info(f" reason: {reason}") logger.info("=" * 50) from ._header_footer import _print_footer diff --git a/jqmc_workflow/lrdmc_ext_workflow.py b/jqmc_workflow/lrdmc_ext_workflow.py index 02602b14..6777ceaf 100644 --- a/jqmc_workflow/lrdmc_ext_workflow.py +++ b/jqmc_workflow/lrdmc_ext_workflow.py @@ -47,13 +47,12 @@ import subprocess from logging import getLogger -from ._output_parser import parse_lrdmc_output from ._setting import ( GFMC_MIN_BIN_BLOCKS, GFMC_MIN_COLLECT_STEPS, GFMC_MIN_WARMUP_STEPS, ) -from ._state import WorkflowStatus +from ._state import WorkflowStatus, read_state from .lrdmc_workflow import LRDMC_Workflow from .workflow import Container, Workflow @@ -388,7 +387,12 @@ def _make_lrdmc_workflow(self, alat): pilot_steps=self.pilot_steps, num_gfmc_projections=self.num_gfmc_projections, max_continuation=self.max_continuation, - cleanup_patterns=self.cleanup_patterns, + # Children do NOT clean up: LRDMC_Ext.run() needs every alat's + # restart.h5 *after* the children complete, for the + # extrapolation step. The parent Container handles cleanup + # recursively (via ``**/`` glob) after extrapolation + # has consumed the files. + cleanup_patterns=None, precision_mode=self.precision_mode, ) enc = Container( @@ -412,6 +416,51 @@ def configure(self) -> dict: "max_continuation": self.max_continuation, } + def can_resume_after_completed(self, proj_dir: str) -> bool: + """Return True if *any* child ``LRDMC_Workflow`` at any alat could + still benefit from more runs. + + ``Container`` consults this before: + + * short-circuiting on a previously completed workflow, and + * running ``_cleanup_files`` (which uses a recursive + ``**/`` glob that would otherwise delete the children's + ``restart.h5`` files that an unconverged child wants to keep). + + Returns True when any of the following hold for the *current* + ``alat_list``: + + * an alat in the list has no recorded ``energy_error`` yet + (e.g. the user extended ``alat_list`` after the prior run, so + this alat has never been executed); or + * an alat's recorded ``energy_error`` exceeds + ``target_error * 1.20``. + + Returns False in fixed-step mode (no target_error) or when + every alat already has an acceptable recorded ``energy_error``. + """ + if self.target_error is None or self.num_gfmc_projections is not None: + return False + for alat in self.alat_list: + alat_dir = os.path.join(proj_dir, f"lrdmc_alat_{alat:.3f}") + try: + result = read_state(alat_dir).get("result", {}) + except Exception as exc: + logger.warning( + f"can_resume_after_completed: cannot read state for " + f"alat={alat} ({exc.__class__.__name__}: {exc}); " + f"requesting resume to recover." + ) + return True + err = result.get("energy_error") + if err is None: + # No result for this alat yet -- definitely need to run it + # (e.g. user just added this value to alat_list). + return True + if err > self.target_error * 1.20: + return True + return False + async def run(self) -> tuple: """Run LRDMC at each alat, then extrapolate to a^2->0. @@ -477,7 +526,7 @@ async def _run_one(enc): logger.error(f"[{enc.label}] failed: {error}") errors.append(str(error)) continue - if status not in ("success", "completed", WorkflowStatus.COMPLETED): + if status != WorkflowStatus.COMPLETED: logger.error(f"[{enc.label}] returned status={status}") errors.append(f"{enc.label}: status={status}") continue @@ -520,27 +569,32 @@ async def _run_one(enc): self.output_values["per_alat_results"] = per_alat_results - # Publish averaged GFMC projections per alat as a TOML-safe - # list of {"alat": float, "nmpm": int} records. A downstream + # Publish GFMC projections per alat as a TOML-safe list of + # ``{"alat": float, "nmpm": int}`` records. A downstream # GFMC_n LRDMC_Ext_Workflow can consume this via ValueFrom and - # pass it back as num_projection_per_measurement (the __init__ - # accepts this list form and normalizes to dict[float, int]). + # pass it back as ``num_projection_per_measurement`` (the + # ``__init__`` accepts this list form and normalizes to + # ``dict[float, int]``). Each child LRDMC_Workflow publishes + # ``num_projection_per_measurement`` in both GFMC_n (user/calib + # input) and GFMC_t (averaged measurement) modes. + out_values_by_alat: dict[float, dict] = { + float(_out_values["alat"]): _out_values + for _enc, _status, _out_files, _out_values, _error in all_results + if _error is None and _status == WorkflowStatus.COMPLETED and _out_values.get("alat") is not None + } + nmpm_per_alat: list[dict] = [] for alat in self.alat_list: - alat_dir = os.path.join(self.project_dir, f"lrdmc_alat_{alat:.3f}") - try: - diag = parse_lrdmc_output(alat_dir) - except Exception: - diag = None - if diag is not None and getattr(diag, "avg_num_projections", None) is not None: - nmpm_per_alat.append( - { - "alat": float(alat), - "nmpm": max(int(round(float(diag.avg_num_projections))), 1), - } - ) - if nmpm_per_alat: - self.output_values["nmpm_per_alat"] = nmpm_per_alat + ov = out_values_by_alat.get(float(alat)) + nmpm_raw = ov.get("num_projection_per_measurement") if ov is not None else None + if nmpm_raw is None: + msg = f"Missing output_values['num_projection_per_measurement'] for alat={alat:.3f} in sub-workflow result." + logger.error(msg) + self.status = WorkflowStatus.FAILED + self.output_values["error"] = msg + return self.status, [], {"error": msg} + nmpm_per_alat.append({"alat": float(alat), "nmpm": max(int(nmpm_raw), 1)}) + self.output_values["nmpm_per_alat"] = nmpm_per_alat self.output_files = restart_chks self.status = WorkflowStatus.COMPLETED @@ -553,25 +607,35 @@ def _extrapolate_energy(self, restart_chks: list[str]): tuple: ``(energy, error)`` or ``(None, None)``. """ - chk_args = " ".join(restart_chks) - cmd = ( - f"jqmc-tool lrdmc extrapolate-energy {chk_args} " - f"-p {self.polynomial_order} " - f"-b {self.num_gfmc_bin_blocks} " - f"-w {self.num_gfmc_warmup_steps} " - f"-c {self.num_gfmc_collect_steps}" - ) - logger.info(f" Running: {cmd}") + cmd = [ + "jqmc-tool", + "lrdmc", + "extrapolate-energy", + *restart_chks, + "-p", + str(self.polynomial_order), + "-b", + str(self.num_gfmc_bin_blocks), + "-w", + str(self.num_gfmc_warmup_steps), + "-c", + str(self.num_gfmc_collect_steps), + ] + logger.info(f" Running: {' '.join(cmd)}") try: result = subprocess.run( cmd, - shell=True, + shell=False, capture_output=True, text=True, + errors="replace", check=True, cwd=self.project_dir, ) return self._parse_extrapolation_output(result.stdout) + except FileNotFoundError as e: + logger.error(f"extrapolate-energy: '{cmd[0]}' not found on PATH ({e})") + return None, None except subprocess.CalledProcessError as e: logger.error(f"extrapolate-energy failed: {e.stderr}") return None, None diff --git a/jqmc_workflow/lrdmc_workflow.py b/jqmc_workflow/lrdmc_workflow.py index c029d4e0..e28ee742 100644 --- a/jqmc_workflow/lrdmc_workflow.py +++ b/jqmc_workflow/lrdmc_workflow.py @@ -60,6 +60,7 @@ estimate_additional_steps, estimate_required_steps, parse_net_time, + read_accumulated_measurement_steps, ) from ._input_generator import generate_input_toml, resolve_with_defaults from ._job import get_num_mpi, load_queue_data @@ -68,7 +69,7 @@ get_num_electrons, parse_survived_walkers_ratio, ) -from ._output_parser import parse_force_table +from ._output_parser import parse_force_table, parse_lrdmc_output from ._setting import ( GFMC_MIN_BIN_BLOCKS, GFMC_MIN_COLLECT_STEPS, @@ -79,6 +80,8 @@ WorkflowStatus, get_estimation, get_job_by_step, + read_state, + reconcile_fetched_jobs_recursive, set_estimation, validate_completion, ) @@ -480,6 +483,28 @@ def configure(self) -> dict: "max_continuation": self.max_continuation, } + def can_resume_after_completed(self, proj_dir: str) -> bool: + """Return True when a re-launch could still reduce ``energy_error`` toward target. + + A prior ``"completed"`` state may have been written by + :meth:`_launch_auto` after exhausting ``max_continuation`` without + meeting ``target_error``. When the user raises ``max_continuation`` + (or tightens ``target_error``) and relaunches, the recorded + ``[result].energy_error`` will still exceed ``target_error*1.20`` + and this method returns True so ``Container`` bypasses its + short-circuit and re-enters the production loop. + + Fixed-step mode (``num_gfmc_projections`` set) has no convergence + criterion and is never resumed automatically. + """ + if self.target_error is None or self.num_gfmc_projections is not None: + return False + result = read_state(proj_dir).get("result", {}) + err = result.get("energy_error") + if err is None: + return False + return err > self.target_error * 1.20 + async def run(self) -> tuple: """Run the LRDMC workflow. @@ -501,6 +526,17 @@ async def run(self) -> tuple: self._ensure_project_dir() _wd = self.project_dir + # Reconcile any orphaned "submitted"/"completed" job records whose + # output file already landed locally with a "Program ends" marker + # (e.g. workflow killed between job completion and fetch-finalize). + # Walks pilot subdirectories (``_pilot_a/``, ``_pilot_b/``) too, + # since each carries its own state file. Without this, Phase A + # would break out at the orphan record and the safety-net energy + # computation would read a stale earlier step. + n_reconciled = reconcile_fetched_jobs_recursive(_wd) + if n_reconciled: + logger.info(f" Reconciled {n_reconciled} job record(s) to 'fetched' from existing output.") + # -- Fixed-step mode --------------------------------------- if self.num_gfmc_projections is not None: return await self._launch_fixed_steps(_wd) @@ -588,7 +624,7 @@ async def _launch_fixed_steps(self, _wd): # Post-process energy (informational only, no convergence check) restart_chk = self._find_restart_chk(_wd) if restart_chk: - energy, error = self._compute_energy(restart_chk, work_dir=_wd, output_file=output_i) + energy, error = self._compute_energy(restart_chk, work_dir=_wd) if energy is not None: self.output_values["energy"] = energy self.output_values["energy_error"] = error @@ -622,7 +658,7 @@ async def _launch_fixed_steps(self, _wd): last_output = step_files[last_run][1] if last_run in step_files else None restart_chk = self._find_restart_chk(_wd) if restart_chk: - energy, error = self._compute_energy(restart_chk, work_dir=_wd, output_file=last_output) + energy, error = self._compute_energy(restart_chk, work_dir=_wd) if energy is not None: self.output_values["energy"] = energy self.output_values["energy_error"] = error @@ -650,6 +686,9 @@ async def _launch_fixed_steps(self, _wd): self.output_values["num_projection_per_measurement"] = self.num_projection_per_measurement else: self.output_values["time_projection_tau"] = self.time_projection_tau + avg_nmpm = self._resolve_avg_nmpm(_wd) + if avg_nmpm is not None: + self.output_values["num_projection_per_measurement"] = avg_nmpm if self.status != WorkflowStatus.FAILED: self.status = WorkflowStatus.COMPLETED @@ -814,7 +853,7 @@ async def _launch_auto(self, _wd): if not restart_chk: raise RuntimeError("No checkpoint found after pilot run. Cannot estimate required steps.") - _, pilot_error = self._compute_energy(restart_chk, work_dir=pilot_b_dir, output_file=output_pb) + _, pilot_error = self._compute_energy(restart_chk, work_dir=pilot_b_dir) if pilot_error is None: raise RuntimeError("Could not parse energy error from pilot run.") @@ -923,13 +962,22 @@ async def _launch_auto(self, _wd): logger.info( f" Target already achieved (cached): {cached_error:.6g} <= {self.target_error * 1.20:.6g} Ha (target*1.20)" ) + # Mode-specific key: avoid writing None (which TOML + # silently drops, breaking downstream readers). + if self._use_gfmc_n: + mode_extras = {"num_projection_per_measurement": self.num_projection_per_measurement} + else: + mode_extras = {"time_projection_tau": self.time_projection_tau} + avg_nmpm = self._resolve_avg_nmpm(_wd) + if avg_nmpm is not None: + mode_extras["num_projection_per_measurement"] = avg_nmpm self.output_values.update( energy=cached_energy, energy_error=cached_error, alat=self.alat, restart_chk=restart_chk or "", estimated_steps=estimated_steps, - num_projection_per_measurement=self.num_projection_per_measurement, + **mode_extras, ) if self.atomic_force and restart_chk: forces = self._compute_force(restart_chk, work_dir=_wd) @@ -970,46 +1018,56 @@ async def _launch_auto(self, _wd): # -- Phase B: re-estimate from accumulated data -- accumulated_measurement = 0 # measurement steps only (excl. warmup) if first_new_run > 1: - cached_accum = estimation.get("accumulated_measurement_steps") - if cached_accum is not None: - accumulated_measurement = int(cached_accum) - else: - accumulated_measurement = (first_new_run - 1) * max(estimated_steps - warmup, 0) - _re_chk = self._find_restart_chk(_wd) - if _re_chk: - _re_energy, _re_error = self._compute_energy(_re_chk, work_dir=_wd) - if _re_energy is not None and _re_error is not None: - if _re_error <= self.target_error * 1.20: - logger.info( - f" Target already met after prior runs: {_re_error:.6g} <= {self.target_error * 1.20:.6g} Ha" - ) - self.output_values.update( - energy=_re_energy, - energy_error=_re_error, - alat=self.alat, - restart_chk=_re_chk, - ) - if self.atomic_force: - forces = self._compute_force(_re_chk, work_dir=_wd) - if forces is not None: - self.output_values["forces"] = forces - first_new_run = self.max_continuation + 1 # skip loop - else: - _additional = estimate_additional_steps( - accumulated_measurement, - _re_error, - self.target_error, - ) - estimated_steps = _additional + warmup - logger.info( - f" Resuming after {first_new_run - 1} prior run(s): " - f"error={_re_error:.6g} Ha > target " - f"{self.target_error:.6g} Ha -> " - f"{estimated_steps} steps " - f"(measurement: {_additional}, warmup: {warmup}, " - f"accumulated measurement: {accumulated_measurement})" - ) + if _re_chk is None: + raise RuntimeError( + f"Phase B: {first_new_run - 1} prior run(s) marked fetched but no restart checkpoint found in {_wd}." + ) + # mcmc_counter in restart.h5 is the only trustworthy source + # for accumulated samples (planned step counts over-count + # when prior runs were cut short by max_time). + actual = read_accumulated_measurement_steps( + os.path.join(_wd, _re_chk), + warmup=warmup, + collect_steps=self.num_gfmc_collect_steps, + ) + if actual is None: + raise RuntimeError(f"Phase B: cannot read mcmc_counter from {_re_chk} in {_wd}.") + accumulated_measurement = actual + + _re_energy, _re_error = self._compute_energy_cached(_re_chk, work_dir=_wd, accumulated=actual) + if _re_energy is None or _re_error is None: + raise RuntimeError( + f"Phase B: compute-energy failed for {_re_chk} in {_wd}. Cannot decide whether to resume or stop." + ) + if _re_error <= self.target_error * 1.20: + logger.info(f" Target already met after prior runs: {_re_error:.6g} <= {self.target_error * 1.20:.6g} Ha") + self.output_values.update( + energy=_re_energy, + energy_error=_re_error, + alat=self.alat, + restart_chk=_re_chk, + ) + if self.atomic_force: + forces = self._compute_force(_re_chk, work_dir=_wd) + if forces is not None: + self.output_values["forces"] = forces + first_new_run = self.max_continuation + 1 # skip loop + else: + _additional = estimate_additional_steps( + accumulated_measurement, + _re_error, + self.target_error, + ) + estimated_steps = _additional + warmup + logger.info( + f" Resuming after {first_new_run - 1} prior run(s): " + f"error={_re_error:.6g} Ha > target " + f"{self.target_error:.6g} Ha -> " + f"{estimated_steps} steps " + f"(measurement: {_additional}, warmup: {warmup}, " + f"accumulated measurement: {accumulated_measurement})" + ) # -- Phase C: production loop -- _prev_run_steps = None @@ -1079,35 +1137,45 @@ async def _launch_auto(self, _wd): step=i, run_id=run_id_i, ) - accumulated_measurement += estimated_steps - warmup _prev_run_steps = estimated_steps last_run = i # -- Side-effects: compute energy from checkpoint (if any) -- restart_chk = self._find_restart_chk(_wd) - energy = error = None - if restart_chk: - energy, error = self._compute_energy(restart_chk, work_dir=_wd, output_file=output_i) - if energy is not None: - self.output_values["energy"] = energy - self.output_values["energy_error"] = error - self.output_values["alat"] = self.alat - self.output_values["restart_chk"] = restart_chk - logger.info(f" LRDMC energy (a={self.alat}): {energy} +- {error} Ha") - if self.atomic_force: - forces = self._compute_force(restart_chk, work_dir=_wd, output_file=output_i) - if forces is not None: - self.output_values["forces"] = forces + if restart_chk is None: + raise RuntimeError(f"Phase C: run {i} completed but no restart checkpoint found in {_wd}.") + # mcmc_counter in restart.h5 is the only trustworthy source for + # accumulated samples (planned step counts over-count when the + # run was cut short by max_time). + actual = read_accumulated_measurement_steps( + os.path.join(_wd, restart_chk), + warmup=warmup, + collect_steps=self.num_gfmc_collect_steps, + ) + if actual is None: + raise RuntimeError(f"Phase C: cannot read mcmc_counter from {restart_chk} in {_wd}.") + accumulated_measurement = actual + energy, error = self._compute_energy_cached(restart_chk, work_dir=_wd, accumulated=actual) + if energy is not None: + self.output_values["energy"] = energy + self.output_values["energy_error"] = error + self.output_values["alat"] = self.alat + self.output_values["restart_chk"] = restart_chk + logger.info(f" LRDMC energy (a={self.alat}): {energy} +- {error} Ha") + if self.atomic_force: + forces = self._compute_force(restart_chk, work_dir=_wd, output_file=output_i) + if forces is not None: + self.output_values["forces"] = forces - set_estimation( - _wd, - last_energy=energy, - last_energy_error=error, - accumulated_measurement_steps=accumulated_measurement, - last_num_gfmc_bin_blocks=self.num_gfmc_bin_blocks, - last_num_gfmc_warmup_steps=self.num_gfmc_warmup_steps, - last_num_gfmc_collect_steps=self.num_gfmc_collect_steps, - ) + set_estimation( + _wd, + last_energy=energy, + last_energy_error=error, + accumulated_measurement_steps=accumulated_measurement, + last_num_gfmc_bin_blocks=self.num_gfmc_bin_blocks, + last_num_gfmc_warmup_steps=self.num_gfmc_warmup_steps, + last_num_gfmc_collect_steps=self.num_gfmc_collect_steps, + ) # -- Termination decision -- single source of truth -- vstatus, vmsg = validate_completion( @@ -1150,7 +1218,7 @@ async def _launch_auto(self, _wd): last_output = step_files[last_run][1] if last_run in step_files else None restart_chk = self._find_restart_chk(_wd) if restart_chk: - energy, error = self._compute_energy(restart_chk, work_dir=_wd, output_file=last_output) + energy, error = self._compute_energy_cached(restart_chk, work_dir=_wd) if energy is not None: self.output_values["energy"] = energy self.output_values["energy_error"] = error @@ -1179,6 +1247,9 @@ async def _launch_auto(self, _wd): self.output_values["num_projection_per_measurement"] = self.num_projection_per_measurement else: self.output_values["time_projection_tau"] = self.time_projection_tau + avg_nmpm = self._resolve_avg_nmpm(_wd) + if avg_nmpm is not None: + self.output_values["num_projection_per_measurement"] = avg_nmpm if self.status != WorkflowStatus.FAILED: self.status = WorkflowStatus.COMPLETED @@ -1186,6 +1257,26 @@ async def _launch_auto(self, _wd): # -- Utility methods ------------------------------------------- + def _resolve_avg_nmpm(self, work_dir: str) -> int | None: + """Parse the GFMC_t output log for the averaged number of projections. + + Returns the rounded ``avg_num_projections`` as an int (>=1), or + None if the diagnostic is unavailable. Used to expose the + averaged nmpm via ``output_values["num_projection_per_measurement"]`` + so that downstream GFMC_n runs can consume it via ``ValueFrom``. + """ + try: + diag = parse_lrdmc_output(work_dir) + except Exception: + return None + avg = getattr(diag, "avg_num_projections", None) if diag is not None else None + if avg is None: + return None + try: + return max(int(round(float(avg))), 1) + except (TypeError, ValueError): + return None + def _find_restart_chk(self, work_dir: str) -> str | None: """Locate the LRDMC restart checkpoint file in *work_dir*.""" for pattern in ["restart.h5", "lrdmc.h5", "*.h5"]: @@ -1194,62 +1285,112 @@ def _find_restart_chk(self, work_dir: str) -> str | None: return os.path.basename(matches[-1]) return None - def _compute_energy(self, restart_chk: str, work_dir: str, output_file: str | None = None): - """Parse energy from *output_file* or run ``jqmc-tool lrdmc compute-energy``. + def _compute_energy_cached(self, restart_chk: str, work_dir: str, accumulated: int | None = None): + """Return (energy, error) using ``[estimation]`` cache when fresh. - When *output_file* is given the energy is read directly from - the ``jqmc`` stdout (``Total Energy: E = ... +- ... Ha.``). - This avoids the overhead of re-running ``jqmc-tool`` when - the post-processing parameters (-b, -w, -c) are the same as - in the input TOML -- which is always the case for a fresh run. + The cache (``last_energy`` / ``last_energy_error`` in + ``workflow_state.toml``) is considered fresh when the recorded + ``accumulated_measurement_steps`` matches the current + ``restart.h5`` ``mcmc_counter`` *and* the post-processing + parameters (``-b``, ``-w``, ``-c``) match the workflow's + current settings. On a hit, no subprocess is launched. - Falls back to ``jqmc-tool`` when *output_file* is *None* or - when stdout parsing fails. + On a miss, :meth:`_compute_energy` is invoked and the cache is + refreshed via :func:`set_estimation` so that subsequent + invocations within the same or later workflow runs short-circuit. + + Args: + restart_chk (str): + Checkpoint filename (basename). + work_dir (str): + Directory in which to run the command. + accumulated (int, optional): + Pre-read ``mcmc_counter`` from ``restart.h5``. Pass when + the caller has already computed it (Phase B / Phase C) to + avoid a redundant HDF5 read. + """ + if accumulated is None: + accumulated = read_accumulated_measurement_steps( + os.path.join(work_dir, restart_chk), + warmup=self.num_gfmc_warmup_steps, + collect_steps=self.num_gfmc_collect_steps, + ) + est = get_estimation(work_dir) + if ( + accumulated is not None + and est.get("last_energy") is not None + and est.get("last_energy_error") is not None + and est.get("accumulated_measurement_steps") == accumulated + and est.get("last_num_gfmc_bin_blocks") == self.num_gfmc_bin_blocks + and est.get("last_num_gfmc_warmup_steps") == self.num_gfmc_warmup_steps + and est.get("last_num_gfmc_collect_steps") == self.num_gfmc_collect_steps + ): + e, err = est["last_energy"], est["last_energy_error"] + logger.info( + f" Energy cached: E = {e} +- {err} Ha " + f"(acc={accumulated}, b={self.num_gfmc_bin_blocks}, " + f"w={self.num_gfmc_warmup_steps}, c={self.num_gfmc_collect_steps})" + ) + return e, err + energy, error = self._compute_energy(restart_chk, work_dir=work_dir) + if energy is not None and accumulated is not None: + set_estimation( + work_dir, + last_energy=energy, + last_energy_error=error, + accumulated_measurement_steps=accumulated, + last_num_gfmc_bin_blocks=self.num_gfmc_bin_blocks, + last_num_gfmc_warmup_steps=self.num_gfmc_warmup_steps, + last_num_gfmc_collect_steps=self.num_gfmc_collect_steps, + ) + return energy, error + + def _compute_energy(self, restart_chk: str, work_dir: str): + """Run ``jqmc-tool lrdmc compute-energy`` against *restart_chk*. + + Always invokes ``jqmc-tool`` so that the returned (energy, error) + carry full numerical precision. Parsing jqmc's printed + ``Total Energy: E = ... +- ... Ha.`` line is lossy (``%.5f`` + formatting), which is unsuitable for values persisted to + ``workflow_state.toml`` or compared against ``target_error``. Args: restart_chk (str): Checkpoint filename (basename). work_dir (str): Directory in which to run the command. - output_file (str, optional): - Stdout filename (basename) of the ``jqmc`` run. Returns: tuple: - ``(energy, error)`` or ``(None, None)``. + ``(energy, error)`` or ``(None, None)`` on failure. """ - # Fast path: parse from jqmc stdout - if output_file is not None: - out_path = os.path.join(work_dir, output_file) - if os.path.isfile(out_path): - try: - with open(out_path) as fh: - text = fh.read() - energy, error = self._parse_energy_output(text) - if energy is not None: - logger.info(f" Energy from {output_file} (jqmc-tool skipped): E = {energy} +- {error} Ha") - return energy, error - except OSError: - pass - - # Fallback: jqmc-tool - cmd = ( - f"jqmc-tool lrdmc compute-energy {restart_chk} " - f"-b {self.num_gfmc_bin_blocks} " - f"-w {self.num_gfmc_warmup_steps} " - f"-c {self.num_gfmc_collect_steps}" - ) - logger.info(f" Running: {cmd}") + cmd = [ + "jqmc-tool", + "lrdmc", + "compute-energy", + restart_chk, + "-b", + str(self.num_gfmc_bin_blocks), + "-w", + str(self.num_gfmc_warmup_steps), + "-c", + str(self.num_gfmc_collect_steps), + ] + logger.info(f" Running: {' '.join(cmd)}") try: result = subprocess.run( cmd, - shell=True, + shell=False, capture_output=True, text=True, + errors="replace", check=True, cwd=work_dir, ) return self._parse_energy_output(result.stdout) + except FileNotFoundError as e: + logger.error(f"compute-energy: '{cmd[0]}' not found on PATH ({e})") + return None, None except subprocess.CalledProcessError as e: logger.error(f"compute-energy failed: {e.stderr}") return None, None @@ -1307,19 +1448,26 @@ def _compute_force(self, restart_chk: str, work_dir: str, output_file: str | Non pass # Fallback: jqmc-tool - cmd = ( - f"jqmc-tool lrdmc compute-force {restart_chk} " - f"-b {self.num_gfmc_bin_blocks} " - f"-w {self.num_gfmc_warmup_steps} " - f"-c {self.num_gfmc_collect_steps}" - ) - logger.info(f" Running: {cmd}") + cmd = [ + "jqmc-tool", + "lrdmc", + "compute-force", + restart_chk, + "-b", + str(self.num_gfmc_bin_blocks), + "-w", + str(self.num_gfmc_warmup_steps), + "-c", + str(self.num_gfmc_collect_steps), + ] + logger.info(f" Running: {' '.join(cmd)}") try: result = subprocess.run( cmd, - shell=True, + shell=False, capture_output=True, text=True, + errors="replace", check=True, cwd=work_dir, ) @@ -1334,6 +1482,9 @@ def _compute_force(self, restart_chk: str, work_dir: str, output_file: str | Non f" Ha/bohr" ) return forces + except FileNotFoundError as e: + logger.error(f"compute-force: '{cmd[0]}' not found on PATH ({e})") + return None except subprocess.CalledProcessError as e: logger.error(f"compute-force failed: {e.stderr}") return None diff --git a/jqmc_workflow/mcmc_workflow.py b/jqmc_workflow/mcmc_workflow.py index 598cfa63..147f65b5 100644 --- a/jqmc_workflow/mcmc_workflow.py +++ b/jqmc_workflow/mcmc_workflow.py @@ -49,6 +49,7 @@ estimate_additional_steps, estimate_required_steps, parse_net_time, + read_accumulated_measurement_steps, ) from ._input_generator import generate_input_toml, resolve_with_defaults from ._job import get_num_mpi, load_queue_data @@ -58,6 +59,8 @@ WorkflowStatus, get_estimation, get_job_by_step, + read_state, + reconcile_fetched_jobs_recursive, set_estimation, validate_completion, ) @@ -340,6 +343,28 @@ def configure(self) -> dict: "max_continuation": self.max_continuation, } + def can_resume_after_completed(self, proj_dir: str) -> bool: + """Return True when a re-launch could still reduce ``energy_error`` toward target. + + A prior ``"completed"`` state may have been written by + :meth:`_launch_auto` after exhausting ``max_continuation`` without + meeting ``target_error``. When the user raises ``max_continuation`` + (or tightens ``target_error``) and relaunches, the recorded + ``[result].energy_error`` will still exceed ``target_error*1.05`` + and this method returns True so ``Container`` bypasses its + short-circuit and re-enters the production loop. + + Fixed-step mode (``num_mcmc_steps`` set) has no convergence + criterion and is never resumed automatically. + """ + if self.target_error is None or self.num_mcmc_steps is not None: + return False + result = read_state(proj_dir).get("result", {}) + err = result.get("energy_error") + if err is None: + return False + return err > self.target_error * 1.05 + async def run(self) -> tuple: """Run the MCMC workflow. @@ -360,6 +385,17 @@ async def run(self) -> tuple: self._ensure_project_dir() _wd = self.project_dir + # Reconcile any orphaned "submitted"/"completed" job records whose + # output file already landed locally with a "Program ends" marker + # (e.g. workflow killed between job completion and fetch-finalize). + # Walks the pilot subdirectory (``_pilot/``) too, since it carries + # its own state file. Without this, Phase A would break out at + # the orphan record and the safety-net energy computation would + # read a stale earlier step. + n_reconciled = reconcile_fetched_jobs_recursive(_wd) + if n_reconciled: + logger.info(f" Reconciled {n_reconciled} job record(s) to 'fetched' from existing output.") + # -- Fixed-step mode --------------------------------------- if self.num_mcmc_steps is not None: return await self._launch_fixed_steps(_wd) @@ -428,7 +464,7 @@ async def _launch_fixed_steps(self, _wd): # Post-process energy (informational only, no convergence check) restart_chk = self._find_restart_chk(_wd) if restart_chk: - energy, error = self._compute_energy(restart_chk, work_dir=_wd, output_file=output_i) + energy, error = self._compute_energy(restart_chk, work_dir=_wd) if energy is not None: self.output_values["energy"] = energy self.output_values["energy_error"] = error @@ -460,7 +496,7 @@ async def _launch_fixed_steps(self, _wd): last_output = step_files[last_run][1] if last_run in step_files else None restart_chk = self._find_restart_chk(_wd) if restart_chk: - energy, error = self._compute_energy(restart_chk, work_dir=_wd, output_file=last_output) + energy, error = self._compute_energy(restart_chk, work_dir=_wd) if energy is not None: self.output_values["energy"] = energy self.output_values["energy_error"] = error @@ -535,7 +571,7 @@ async def _launch_auto(self, _wd): if not restart_chk: raise RuntimeError("No checkpoint found after pilot run. Cannot estimate required steps.") - _, pilot_error = self._compute_energy(restart_chk, work_dir=pilot_dir, output_file=output_0) + _, pilot_error = self._compute_energy(restart_chk, work_dir=pilot_dir) if pilot_error is None: raise RuntimeError("Could not parse energy error from pilot run.") @@ -677,45 +713,54 @@ async def _launch_auto(self, _wd): # -- Phase B: re-estimate from accumulated data -- accumulated_measurement = 0 # measurement steps only (excl. warmup) if first_new_run > 1: - cached_accum = estimation.get("accumulated_measurement_steps") - if cached_accum is not None: - accumulated_measurement = int(cached_accum) - else: - accumulated_measurement = (first_new_run - 1) * max(estimated_steps - warmup, 0) - _re_chk = self._find_restart_chk(_wd) - if _re_chk: - _re_energy, _re_error = self._compute_energy(_re_chk, work_dir=_wd) - if _re_energy is not None and _re_error is not None: - if _re_error <= self.target_error * 1.05: - logger.info( - f" Target already met after prior runs: {_re_error:.6g} <= {self.target_error * 1.05:.6g} Ha" - ) - self.output_values.update( - energy=_re_energy, - energy_error=_re_error, - restart_chk=_re_chk, - ) - if self.atomic_force: - forces = self._compute_force(_re_chk, work_dir=_wd) - if forces is not None: - self.output_values["forces"] = forces - first_new_run = self.max_continuation + 1 # skip loop - else: - _additional = estimate_additional_steps( - accumulated_measurement, - _re_error, - self.target_error, - ) - estimated_steps = _additional + warmup - logger.info( - f" Resuming after {first_new_run - 1} prior run(s): " - f"error={_re_error:.6g} Ha > target " - f"{self.target_error:.6g} Ha -> " - f"{estimated_steps} steps " - f"(measurement: {_additional}, warmup: {warmup}, " - f"accumulated measurement: {accumulated_measurement})" - ) + if _re_chk is None: + raise RuntimeError( + f"Phase B: {first_new_run - 1} prior run(s) marked fetched but no restart checkpoint found in {_wd}." + ) + # mcmc_counter in restart.h5 is the only trustworthy source + # for accumulated samples (planned step counts over-count + # when prior runs were cut short by max_time). + actual = read_accumulated_measurement_steps( + os.path.join(_wd, _re_chk), + warmup=warmup, + ) + if actual is None: + raise RuntimeError(f"Phase B: cannot read mcmc_counter from {_re_chk} in {_wd}.") + accumulated_measurement = actual + + _re_energy, _re_error = self._compute_energy_cached(_re_chk, work_dir=_wd, accumulated=actual) + if _re_energy is None or _re_error is None: + raise RuntimeError( + f"Phase B: compute-energy failed for {_re_chk} in {_wd}. Cannot decide whether to resume or stop." + ) + if _re_error <= self.target_error * 1.05: + logger.info(f" Target already met after prior runs: {_re_error:.6g} <= {self.target_error * 1.05:.6g} Ha") + self.output_values.update( + energy=_re_energy, + energy_error=_re_error, + restart_chk=_re_chk, + ) + if self.atomic_force: + forces = self._compute_force(_re_chk, work_dir=_wd) + if forces is not None: + self.output_values["forces"] = forces + first_new_run = self.max_continuation + 1 # skip loop + else: + _additional = estimate_additional_steps( + accumulated_measurement, + _re_error, + self.target_error, + ) + estimated_steps = _additional + warmup + logger.info( + f" Resuming after {first_new_run - 1} prior run(s): " + f"error={_re_error:.6g} Ha > target " + f"{self.target_error:.6g} Ha -> " + f"{estimated_steps} steps " + f"(measurement: {_additional}, warmup: {warmup}, " + f"accumulated measurement: {accumulated_measurement})" + ) # -- Phase C: production loop -- _prev_run_steps = None @@ -785,33 +830,42 @@ async def _launch_auto(self, _wd): run_id=run_id_i, ) step_files[i] = (input_i, output_i, run_id_i) - accumulated_measurement += estimated_steps - warmup _prev_run_steps = estimated_steps last_run = i # -- Side-effects: compute energy from checkpoint (if any) -- restart_chk = self._find_restart_chk(_wd) - energy = error = None - if restart_chk: - energy, error = self._compute_energy(restart_chk, work_dir=_wd, output_file=output_i) - if energy is not None: - self.output_values["energy"] = energy - self.output_values["energy_error"] = error - self.output_values["restart_chk"] = restart_chk - logger.info(f" MCMC energy: {energy} +- {error} Ha") - if self.atomic_force: - forces = self._compute_force(restart_chk, work_dir=_wd, output_file=output_i) - if forces is not None: - self.output_values["forces"] = forces + if restart_chk is None: + raise RuntimeError(f"Phase C: run {i} completed but no restart checkpoint found in {_wd}.") + # mcmc_counter in restart.h5 is the only trustworthy source for + # accumulated samples (planned step counts over-count when the + # run was cut short by max_time). + actual = read_accumulated_measurement_steps( + os.path.join(_wd, restart_chk), + warmup=warmup, + ) + if actual is None: + raise RuntimeError(f"Phase C: cannot read mcmc_counter from {restart_chk} in {_wd}.") + accumulated_measurement = actual + energy, error = self._compute_energy_cached(restart_chk, work_dir=_wd, accumulated=actual) + if energy is not None: + self.output_values["energy"] = energy + self.output_values["energy_error"] = error + self.output_values["restart_chk"] = restart_chk + logger.info(f" MCMC energy: {energy} +- {error} Ha") + if self.atomic_force: + forces = self._compute_force(restart_chk, work_dir=_wd, output_file=output_i) + if forces is not None: + self.output_values["forces"] = forces - set_estimation( - _wd, - last_energy=energy, - last_energy_error=error, - accumulated_measurement_steps=accumulated_measurement, - last_num_mcmc_bin_blocks=self.num_mcmc_bin_blocks, - last_num_mcmc_warmup_steps=self.num_mcmc_warmup_steps, - ) + set_estimation( + _wd, + last_energy=energy, + last_energy_error=error, + accumulated_measurement_steps=accumulated_measurement, + last_num_mcmc_bin_blocks=self.num_mcmc_bin_blocks, + last_num_mcmc_warmup_steps=self.num_mcmc_warmup_steps, + ) # -- Termination decision -- single source of truth -- vstatus, vmsg = validate_completion( @@ -854,7 +908,7 @@ async def _launch_auto(self, _wd): last_output = step_files[last_run][1] if last_run in step_files else None restart_chk = self._find_restart_chk(_wd) if restart_chk: - energy, error = self._compute_energy(restart_chk, work_dir=_wd, output_file=last_output) + energy, error = self._compute_energy_cached(restart_chk, work_dir=_wd) if energy is not None: self.output_values["energy"] = energy self.output_values["energy_error"] = error @@ -892,53 +946,107 @@ def _find_restart_chk(self, work_dir: str) -> str | None: return os.path.basename(matches[-1]) return None - def _compute_energy(self, restart_chk: str, work_dir: str, output_file: str | None = None): - """Parse energy from *output_file* or run ``jqmc-tool mcmc compute-energy``. + def _compute_energy_cached(self, restart_chk: str, work_dir: str, accumulated: int | None = None): + """Return (energy, error) using ``[estimation]`` cache when fresh. + + The cache (``last_energy`` / ``last_energy_error`` in + ``workflow_state.toml``) is considered fresh when the recorded + ``accumulated_measurement_steps`` matches the current + ``restart.h5`` ``mcmc_counter`` *and* the post-processing + parameters (``-b``, ``-w``) match the workflow's current + settings. On a hit, no subprocess is launched. - When *output_file* is given the energy is read directly from - the ``jqmc`` stdout (``Total Energy: E = ... +- ... Ha.``). - Falls back to ``jqmc-tool`` when *output_file* is *None* or - when stdout parsing fails. + On a miss, :meth:`_compute_energy` is invoked and the cache is + refreshed via :func:`set_estimation` so that subsequent + invocations within the same or later workflow runs short-circuit. + + Args: + restart_chk (str): + Checkpoint filename (basename). + work_dir (str): + Directory in which to run the command. + accumulated (int, optional): + Pre-read ``mcmc_counter`` from ``restart.h5``. Pass when + the caller has already computed it (Phase B / Phase C) to + avoid a redundant HDF5 read. + """ + if accumulated is None: + accumulated = read_accumulated_measurement_steps( + os.path.join(work_dir, restart_chk), + warmup=self.num_mcmc_warmup_steps, + ) + est = get_estimation(work_dir) + if ( + accumulated is not None + and est.get("last_energy") is not None + and est.get("last_energy_error") is not None + and est.get("accumulated_measurement_steps") == accumulated + and est.get("last_num_mcmc_bin_blocks") == self.num_mcmc_bin_blocks + and est.get("last_num_mcmc_warmup_steps") == self.num_mcmc_warmup_steps + ): + e, err = est["last_energy"], est["last_energy_error"] + logger.info( + f" Energy cached: E = {e} +- {err} Ha " + f"(acc={accumulated}, b={self.num_mcmc_bin_blocks}, " + f"w={self.num_mcmc_warmup_steps})" + ) + return e, err + energy, error = self._compute_energy(restart_chk, work_dir=work_dir) + if energy is not None and accumulated is not None: + set_estimation( + work_dir, + last_energy=energy, + last_energy_error=error, + accumulated_measurement_steps=accumulated, + last_num_mcmc_bin_blocks=self.num_mcmc_bin_blocks, + last_num_mcmc_warmup_steps=self.num_mcmc_warmup_steps, + ) + return energy, error + + def _compute_energy(self, restart_chk: str, work_dir: str): + """Run ``jqmc-tool mcmc compute-energy`` against *restart_chk*. + + Always invokes ``jqmc-tool`` so that the returned (energy, error) + carry full numerical precision. Parsing jqmc's printed + ``Total Energy: E = ... +- ... Ha.`` line is lossy (``%.5f`` + formatting), which is unsuitable for values persisted to + ``workflow_state.toml`` or compared against ``target_error``. Args: restart_chk (str): Checkpoint filename (basename). work_dir (str): Directory in which to run the command. - output_file (str, optional): - Stdout filename (basename) of the ``jqmc`` run. Returns: tuple: - ``(energy, error)`` or ``(None, None)``. + ``(energy, error)`` or ``(None, None)`` on failure. """ - # Fast path: parse from jqmc stdout - if output_file is not None: - out_path = os.path.join(work_dir, output_file) - if os.path.isfile(out_path): - try: - with open(out_path) as fh: - text = fh.read() - energy, error = self._parse_energy_output(text) - if energy is not None: - logger.info(f" Energy from {output_file} (jqmc-tool skipped): E = {energy} +- {error} Ha") - return energy, error - except OSError: - pass - - # Fallback: jqmc-tool - cmd = f"jqmc-tool mcmc compute-energy {restart_chk} -b {self.num_mcmc_bin_blocks} -w {self.num_mcmc_warmup_steps}" - logger.info(f" Running: {cmd}") + cmd = [ + "jqmc-tool", + "mcmc", + "compute-energy", + restart_chk, + "-b", + str(self.num_mcmc_bin_blocks), + "-w", + str(self.num_mcmc_warmup_steps), + ] + logger.info(f" Running: {' '.join(cmd)}") try: result = subprocess.run( cmd, - shell=True, + shell=False, capture_output=True, text=True, + errors="replace", check=True, cwd=work_dir, ) return self._parse_energy_output(result.stdout) + except FileNotFoundError as e: + logger.error(f"compute-energy: '{cmd[0]}' not found on PATH ({e})") + return None, None except subprocess.CalledProcessError as e: logger.error(f"compute-energy failed: {e.stderr}") return None, None @@ -996,14 +1104,24 @@ def _compute_force(self, restart_chk: str, work_dir: str, output_file: str | Non pass # Fallback: jqmc-tool - cmd = f"jqmc-tool mcmc compute-force {restart_chk} -b {self.num_mcmc_bin_blocks} -w {self.num_mcmc_warmup_steps}" - logger.info(f" Running: {cmd}") + cmd = [ + "jqmc-tool", + "mcmc", + "compute-force", + restart_chk, + "-b", + str(self.num_mcmc_bin_blocks), + "-w", + str(self.num_mcmc_warmup_steps), + ] + logger.info(f" Running: {' '.join(cmd)}") try: result = subprocess.run( cmd, - shell=True, + shell=False, capture_output=True, text=True, + errors="replace", check=True, cwd=work_dir, ) @@ -1018,6 +1136,9 @@ def _compute_force(self, restart_chk: str, work_dir: str, output_file: str | Non f" Ha/bohr" ) return forces + except FileNotFoundError as e: + logger.error(f"compute-force: '{cmd[0]}' not found on PATH ({e})") + return None except subprocess.CalledProcessError as e: logger.error(f"compute-force failed: {e.stderr}") return None diff --git a/jqmc_workflow/template/machine_data.yaml b/jqmc_workflow/template/machine_data.yaml index 6e583040..339e91ed 100644 --- a/jqmc_workflow/template/machine_data.yaml +++ b/jqmc_workflow/template/machine_data.yaml @@ -1,8 +1,27 @@ # Machine definitions for jqmc-workflow # Edit this file to match your environment. -# Each machine needs at least: machine_type (local or remote), queuing (true or false), workspace_root (path). -# Remote machines also require: ssh_host (Host alias in ~/.ssh/config). -# The top-level key (e.g., "my-cluster") is a nickname; it does NOT have to match the SSH host. +# +# Required fields for every machine: +# machine_type : "local" or "remote" +# queuing : true or false +# workspace_root : absolute path where job working dirs live +# jobsubmit : command used to invoke the submit script (required +# even when queuing=false -- for a plain local run use +# "bash" or "sh"; for a scheduler use "qsub" / "sbatch") +# +# Required additionally when queuing: true: +# jobcheck : command that lists jobs in the queue (e.g. "qstat") +# jobdel : command that cancels a job by id (e.g. "qdel") +# jobnum_index : 0-based token index of the job-id field in +# jobsubmit's stdout (qsub prints "1234.host" -> 0; +# sbatch prints "Submitted batch job 1234" -> 3) +# +# Required additionally when machine_type: remote: +# ssh_host : Host alias in ~/.ssh/config (the top-level YAML key +# above is a nickname and does NOT need to match) +# +# Optional: +# jobacct : scheduler accounting command (e.g. "qstat -fx") # Local execution without a batch scheduler (synchronous). # queuing: false -> bash runs submit.sh synchronously; no PID tracking needed. @@ -10,7 +29,7 @@ localhost: machine_type: local queuing: false workspace_root: /home/username/jqmc_work - jobsubmit: "bash" + jobsubmit: "bash" # required: command that runs the submit script # Local execution without a batch scheduler (asynchronous / background). # queuing: true -> bash launches submit.sh in the background and prints PID; diff --git a/jqmc_workflow/vmc_workflow.py b/jqmc_workflow/vmc_workflow.py index 40bdb7c4..63a2b15c 100644 --- a/jqmc_workflow/vmc_workflow.py +++ b/jqmc_workflow/vmc_workflow.py @@ -55,6 +55,7 @@ WorkflowStatus, get_estimation, get_job_by_step, + reconcile_fetched_jobs_recursive, set_estimation, validate_completion, ) @@ -63,6 +64,39 @@ logger = getLogger("jqmc-workflow").getChild(__name__) +def _last_opt_energy_from_log(output_file: str) -> tuple[float | None, float | None]: + """Return ``(energy, energy_error)`` of the last VMC opt step in *output_file*. + + Single source of truth shared by :meth:`VMC_Workflow._parse_output` + and :meth:`VMC_Workflow._parse_last_opt_energy`. Both used to keep + their own copy of the ``E = ... +- ...`` regex, which drifted + independently and silently dropped ``nan``/``inf`` lines. This + helper instead delegates to the canonical + :func:`_output_parser._parse_vmc_log_text` parser so any future + format change lives in exactly one place. + + A ``nan``/``inf`` energy is returned as a (non-finite) float -- not + ``None`` -- so the caller's ``math.isfinite`` check in + :func:`validate_completion` can flag diverged runs. + """ + try: + with open(output_file, errors="replace") as f: + text = f.read() + except OSError: + return None, None + from ._output_parser import _parse_vmc_log_text + + steps = _parse_vmc_log_text(text) + # Prefer the last opt-step block that actually contains an ``E =`` + # line; the very last block may be a header-only stub from a + # partial write. Note: ``nan`` is not ``None`` so a diverged final + # step is still selected, which is the whole point. + for s in reversed(steps): + if s.energy is not None and s.energy_error is not None: + return s.energy, s.energy_error + return None, None + + class VMC_Workflow(Workflow): r"""VMC (Variational Monte Carlo) Jastrow / orbital optimisation workflow. @@ -469,6 +503,17 @@ async def run(self) -> tuple: self._ensure_project_dir() _wd = self.project_dir + # Reconcile any orphaned "submitted"/"completed" job records whose + # output file already landed locally with a "Program ends" marker + # (e.g. workflow killed between job completion and fetch-finalize). + # Walks the pilot subdirectory (``_pilot/``) too, since it carries + # its own state file. SNR / slope convergence checks parse those + # output files, so a stale "submitted" record would otherwise hide + # the latest data. + n_reconciled = reconcile_fetched_jobs_recursive(_wd) + if n_reconciled: + logger.info(f" Reconciled {n_reconciled} job record(s) to 'fetched' from existing output.") + # -- Fixed-step mode --------------------------------------- if self.num_mcmc_steps is not None: return await self._launch_fixed_steps(_wd) @@ -545,6 +590,12 @@ async def _launch_fixed_steps(self, _wd): logger.info(f" VMC production run {i}/{self.max_continuation} completed.") + # Refresh output_values["energy"] from THIS iteration's log + # before validating -- otherwise validate_completion sees a + # stale or unset energy and the non-finite check at + # _state.py:387 silently passes for diverged runs. + self._parse_output(os.path.join(_wd, output_i)) + # -- Abnormal-termination guard (single source of truth) -- # target_error=None -> only Program-ends / non-finite-energy # checks are active. VMC's SNR/slope convergence is decided @@ -805,6 +856,12 @@ async def _launch_auto(self, _wd): logger.info(f" VMC production run {i}/{self.max_continuation} completed.") + # Refresh output_values["energy"] from THIS iteration's log + # before validating -- otherwise validate_completion sees a + # stale or unset energy and the non-finite check at + # _state.py:387 silently passes for diverged runs. + self._parse_output(os.path.join(_wd, output_i)) + # -- Abnormal-termination guard (single source of truth) -- # target_error=None -> only Program-ends / non-finite-energy # checks; SNR/slope convergence is evaluated separately below. @@ -960,27 +1017,19 @@ def _find_restart_chk(self, work_dir: str) -> str | None: # -- Output parsing -------------------------------------------- def _parse_output(self, output_file=None): - """Extract the last optimization energy from *output_file*.""" - if output_file is None: - return - if not os.path.isfile(output_file): - return + """Extract the last optimization step's energy from *output_file*. - energy_pattern = re.compile(r"E\s*=\s*([+-]?\d+\.\d+(?:[eE][+-]?\d+)?)\s*\+\-\s*(\d+\.\d+(?:[eE][+-]?\d+)?)") - last_match = None - try: - with open(output_file) as f: - for line in f: - m = energy_pattern.search(line) - if m: - last_match = m - except Exception: + Delegates to :func:`_output_parser._parse_vmc_log_text` so there + is a single source of truth for VMC log parsing -- any future + format change (or fix like nan/inf support) lives in one place. + """ + if output_file is None or not os.path.isfile(output_file): return - - if last_match: - self.output_values["energy"] = float(last_match.group(1)) - self.output_values["energy_error"] = float(last_match.group(2)) - logger.info(f" VMC energy: {self.output_values['energy']} +- {self.output_values['energy_error']} Ha") + e, err = _last_opt_energy_from_log(output_file) + if e is not None and err is not None: + self.output_values["energy"] = e + self.output_values["energy_error"] = err + logger.info(f" VMC energy: {e} +- {err} Ha") @staticmethod def _parse_all_snr(output_file): @@ -1022,13 +1071,18 @@ def _parse_all_energies(output_file: str) -> list[tuple[float, float]]: if not os.path.isfile(output_file): return [] try: - with open(output_file) as f: + with open(output_file, errors="replace") as f: text = f.read() from ._output_parser import _parse_vmc_log_text steps = _parse_vmc_log_text(text) return [(s.energy, s.energy_error) for s in steps if s.energy is not None and s.energy_error is not None] + except OSError as exc: + logger.warning(f"_parse_all_energies: cannot read {output_file}: {exc}") + return [] except Exception: + # Log unexpected parser failures rather than swallowing silently. + logger.exception(f"_parse_all_energies: unexpected error parsing {output_file}") return [] @staticmethod @@ -1056,6 +1110,11 @@ def _fit_energy_slope( E = np.asarray(energies, dtype=float) sigma = np.asarray(energy_errors, dtype=float) + # Replace non-positive sigmas with the median positive sigma so + # they get a finite (non-inf) weight rather than dividing by zero. + positive = sigma[sigma > 0] + floor = float(np.median(positive)) if positive.size else 1.0 + sigma = np.where(sigma > 0, sigma, floor) w = 1.0 / sigma**2 k = np.arange(len(E), dtype=float) @@ -1072,29 +1131,14 @@ def _fit_energy_slope( @staticmethod def _parse_last_opt_energy(output_file): - """Parse the last ``E = +- `` from a VMC output file. - - Extracts the energy from the *last* optimization step, which - reflects the optimized wavefunction quality. + """Parse the last optimization step's energy from a VMC output file. Returns: tuple: - ``(energy, error)`` or ``(None, None)``. + ``(energy, error)`` or ``(None, None)``. ``nan``/``inf`` + are returned as :class:`float` (not ``None``), so callers + can apply ``math.isfinite`` to detect diverged runs. """ if not os.path.isfile(output_file): return None, None - - energy_pattern = re.compile(r"E\s*=\s*([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?)\s*\+\-\s*(\d+\.?\d*(?:[eE][+-]?\d+)?)") - last_match = None - try: - with open(output_file) as f: - for line in f: - m = energy_pattern.search(line) - if m: - last_match = m - except Exception: - return None, None - - if last_match: - return float(last_match.group(1)), float(last_match.group(2)) - return None, None + return _last_opt_energy_from_log(output_file) diff --git a/jqmc_workflow/wf_workflow.py b/jqmc_workflow/wf_workflow.py index 4590e21d..0552aab6 100644 --- a/jqmc_workflow/wf_workflow.py +++ b/jqmc_workflow/wf_workflow.py @@ -40,7 +40,6 @@ # POSSIBILITY OF SUCH DAMAGE. import os -import shlex import subprocess from logging import getLogger @@ -132,8 +131,8 @@ def __init__( raise ValueError(f"ao_conv_to must be None, 'cart', or 'sphe', got {ao_conv_to!r}") self.ao_conv_to = ao_conv_to - def _build_command(self) -> str: - """Build the ``jqmc-tool trexio convert-to`` CLI command.""" + def _build_command(self) -> list[str]: + """Build the ``jqmc-tool trexio convert-to`` CLI command (argv list).""" cmd = ["jqmc-tool", "trexio", "convert-to", self.trexio_file] cmd += ["-o", self.hamiltonian_file] @@ -154,7 +153,7 @@ def _build_command(self) -> str: if self.ao_conv_to is not None: cmd += ["--ao-conv-to", str(self.ao_conv_to)] - return shlex.join(cmd) + return cmd def configure(self) -> dict: """Validate parameters and return configuration summary.""" @@ -179,20 +178,25 @@ async def run(self) -> tuple: _wd = self.project_dir command = self._build_command() - logger.info(f" Running: {command}") + logger.info(f" Running: {' '.join(command)}") try: result = subprocess.run( command, - shell=True, + shell=False, capture_output=True, text=True, + errors="replace", check=True, cwd=_wd, ) logger.info(result.stdout) if result.stderr: logger.warning(f"stderr: {result.stderr}") + except FileNotFoundError as e: + logger.error(f"Command failed: '{command[0]}' not found on PATH ({e})") + self.status = WorkflowStatus.FAILED + return self.status, [], {} except subprocess.CalledProcessError as e: logger.error(f"Command failed (rc={e.returncode}): {e.stderr}") self.status = WorkflowStatus.FAILED diff --git a/jqmc_workflow/workflow.py b/jqmc_workflow/workflow.py index 4cbedcc4..68f0574e 100644 --- a/jqmc_workflow/workflow.py +++ b/jqmc_workflow/workflow.py @@ -271,12 +271,27 @@ def _ensure_project_dir(self): if self.project_dir is None: self.project_dir = os.path.abspath(os.getcwd()) + # Protected file basenames that ``_cleanup_files`` must never delete, + # regardless of what the user puts in ``cleanup_patterns``. Losing + # any of these breaks workflow state, job history, or resume. + _PROTECTED_CLEANUP_BASENAMES = frozenset( + { + "workflow_state.toml", + "workflow_state.toml.tmp", + } + ) + def _cleanup_files(self): """Delete files matching *cleanup_patterns* from local and remote. Local files are always removed. Remote files are removed only when the workflow targets a remote machine (``server_machine_name`` is set and not ``"localhost"``). + + Protected files (``workflow_state.toml`` and its atomic-write + ``.tmp`` sibling) are *never* deleted, even when matched by an + over-broad pattern like ``"*.toml"`` -- losing the state file + would break job history and resume. """ if not self.cleanup_patterns: return @@ -292,16 +307,24 @@ def _cleanup_files(self): for pattern in self.cleanup_patterns: for fpath in sorted(_glob.glob(os.path.join(work_dir, "**", pattern), recursive=True)): - if os.path.isfile(fpath): - os.remove(fpath) - logger.info(f" Cleanup: removed local file {os.path.relpath(fpath, work_dir)}") + if not os.path.isfile(fpath): + continue + if os.path.basename(fpath) in self._PROTECTED_CLEANUP_BASENAMES: + logger.warning(f" Cleanup: refusing to delete protected file {os.path.relpath(fpath, work_dir)}") + continue + os.remove(fpath) + logger.info(f" Cleanup: removed local file {os.path.relpath(fpath, work_dir)}") return from ._transfer import Data_transfer dt = Data_transfer(server_machine_name) try: - dt.remove_objects(patterns=self.cleanup_patterns, work_dir=work_dir) + dt.remove_objects( + patterns=self.cleanup_patterns, + work_dir=work_dir, + protected_basenames=self._PROTECTED_CLEANUP_BASENAMES, + ) except Exception: dt.ssh_close() raise @@ -310,9 +333,12 @@ def _cleanup_files(self): # -- configure / run (new primary interface) --------------------- def configure(self) -> dict: - """Validate parameters and generate inputs (no execution). + """Return a summary dict of the workflow's parameters (no side effects). - Override in subclass. Returns a summary dict. + Concrete workflows override this to expose their key parameters + for logging / inspection. Parameter *validation* happens in + ``__init__`` (so that invalid configs fail before any I/O), and + input-file generation happens lazily inside ``run()``. """ return {} @@ -328,6 +354,20 @@ async def run(self) -> tuple: self._ensure_project_dir() return self.status, self.output_files, self.output_values + def can_resume_after_completed(self, proj_dir: str) -> bool: + """Return True if a re-launch from a ``"completed"`` state could improve the result. + + ``Container`` consults this before short-circuiting on a + previously completed workflow. Subclasses with a target-error + convergence criterion (LRDMC, MCMC) override this to allow a + bumped ``max_continuation`` or tightened ``target_error`` to + actually re-trigger production runs, instead of silently + accepting the prior under-converged result. + + Default: False (the workflow is genuinely done). + """ + return False + # -- Full lifecycle (backward-compatible) ---------------------- async def async_launch(self): @@ -337,6 +377,11 @@ async def async_launch(self): return await self.run() def launch(self): + """Synchronous entry point: ``asyncio.run(self.async_launch())``. + + Not callable from an already-running event loop (e.g. Jupyter); + use ``await self.async_launch()`` there, or install ``nest_asyncio``. + """ return asyncio.run(self.async_launch()) # -- Phased execution (MCP interactive mode) ------------------- @@ -374,6 +419,9 @@ async def async_submit(self, action: str = "run") -> dict: require_action(action, self.phase, self.status) self._ensure_project_dir() self.configure() + # Flip self.status so subsequent require_action / async_poll + # callers see the workflow as RUNNING rather than PENDING. + self.status = WorkflowStatus.RUNNING self._bg_task = asyncio.create_task(self.run()) return {"status": "submitted", "project_dir": self.project_dir} @@ -390,6 +438,10 @@ async def async_poll(self) -> dict: if not self._bg_task.done(): summary = get_workflow_summary(self.project_dir) if self.project_dir else {} return {"status": "running", **summary} + # Task.exception() raises CancelledError on a cancelled task, so + # check cancellation BEFORE inspecting the exception. + if self._bg_task.cancelled(): + return {"status": "cancelled"} if self._bg_task.exception() is not None: return {"status": "failed", "error": str(self._bg_task.exception())} return {"status": "completed"} @@ -403,7 +455,8 @@ async def async_collect(self) -> dict: Raises: RuntimeError: - If the workflow was not submitted or is still running. + If the workflow was not submitted, was cancelled, or is + still running. Exception: Re-raises the original exception if the workflow failed. """ @@ -411,6 +464,8 @@ async def async_collect(self) -> dict: raise RuntimeError("No workflow has been submitted. Call async_submit() first.") if not self._bg_task.done(): raise RuntimeError("Workflow is still running. Call async_poll() to check status.") + if self._bg_task.cancelled(): + raise RuntimeError("Workflow was cancelled before completion.") exc = self._bg_task.exception() if exc is not None: raise exc @@ -529,6 +584,12 @@ async def _submit_and_wait( if recorded.get("status") == "submitted": stored_job_id = recorded.get("job_id") + if not stored_job_id: + raise RuntimeError( + f"State has step in 'submitted' status but no job_id for {input_file}. " + f"Edit workflow_state.toml (e.g. remove the malformed record or set status='cancelled') " + f"to recover." + ) logger.info(f" Resuming previously submitted job {stored_job_id}") job = self._make_job(input_file, output_file, queue_label=queue_label, run_id=run_id) try: @@ -699,12 +760,24 @@ def __init__( # -- Preparation ----------------------------------------------- def _prepare(self): - """Create project dir, copy input files, write initial state.""" + """Create project dir, copy input files, write initial state. + + Re-entry behaviour: when ``existing_status`` is ``completed`` or + ``running``, input files in *project_dir* are left intact and no + new state is created. ``async_launch`` will subsequently decide + whether to short-circuit (default for ``completed``) or resume + (when the inner workflow opts in via + :meth:`Workflow.can_resume_after_completed`). + """ state = read_state(self.project_dir) existing_status = state.get("workflow", {}).get("status", "") if existing_status in ("completed", "running"): - logger.info(f"[{self.label}] Already {existing_status}. Delete project dir to restart from scratch.") + logger.info( + f"[{self.label}] Existing project dir with status='{existing_status}' " + f"found; will short-circuit or resume depending on workflow policy. " + f"Delete project dir to force a clean restart." + ) return if os.path.isdir(self.project_dir): @@ -816,19 +889,59 @@ def _validate_input_files(self, proj: str): ) def _compute_input_fingerprints(self) -> dict[str, dict]: - """Return ``{basename: {sha256: hex_digest}}`` for each resolved input file.""" + """Return ``{basename: {sha256: hex_digest | "missing"}}`` per input file. + + For each entry in ``self.input_files``: + + * ``FileFrom`` / ``ValueFrom`` placeholders are skipped (they have + no on-disk path yet -- Launcher resolves them before launch; + direct ``.launch()`` may still hold placeholders). + * Regular files are hashed in 1 MiB chunks. + * Directories are hashed by walking their contents in sorted + order -- each file's relative path and content are folded into + the hash so the digest changes when any nested file changes. + * Missing source paths are recorded as ``{"sha256": "missing"}`` + so that :meth:`_check_input_staleness` can distinguish + "input never existed" from "input was deleted since last run". + """ fingerprints: dict[str, dict] = {} for i, src in enumerate(self.input_files): + if _is_dependency(src): + continue src = str(src) if not os.path.isabs(src): src = os.path.join(self.root_dir, src) key = self._dst_basename(src, self.rename_input_files, i) - if os.path.exists(src): + if os.path.isfile(src): h = hashlib.sha256() with open(src, "rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) fingerprints[key] = {"sha256": h.hexdigest()} + elif os.path.isdir(src): + # Walk deterministically (sorted dirs and files) so the + # digest is reproducible across runs. Folding the + # relative path into the hash makes additions, removals, + # and renames all visible. + h = hashlib.sha256() + for root, dirs, files in os.walk(src): + dirs.sort() + for name in sorted(files): + p = os.path.join(root, name) + rel = os.path.relpath(p, src).encode("utf-8", errors="replace") + h.update(rel) + h.update(b"\0") + try: + with open(p, "rb") as f: + for chunk in iter(lambda: f.read(1 << 20), b""): + h.update(chunk) + except OSError: + # Unreadable entry -- fold a marker in so the + # digest still changes when readability flips. + h.update(b"") + fingerprints[key] = {"sha256": h.hexdigest()} + else: + fingerprints[key] = {"sha256": "missing"} return fingerprints def _check_input_staleness(self, proj: str) -> bool: @@ -872,20 +985,30 @@ async def async_launch(self): f"Delete '{self.dirname}/' to re-run with the updated inputs." ) - # Record input-file fingerprints after staleness check but - # before any execution, so that even interrupted runs have a - # baseline for the next invocation. - set_input_fingerprints(proj, self._compute_input_fingerprints()) - if prev_status == "completed": - logger.info(f"[{self.label}] Already completed, no re-run.") - self.status = WorkflowStatus.COMPLETED - self._collect_outputs() - return self.status, self.output_files, self.output_values - - # Validate required files before running. + if self.workflow.can_resume_after_completed(proj): + logger.info( + f"[{self.label}] Previously 'completed' but workflow indicates " + f"the result can still be improved (target not yet met); resuming." + ) + else: + logger.info(f"[{self.label}] Already completed, no re-run.") + self.status = WorkflowStatus.COMPLETED + self._collect_outputs() + # Record fingerprints even on short-circuit so the next + # launch's staleness check has an up-to-date baseline. + set_input_fingerprints(proj, self._compute_input_fingerprints()) + return self.status, self.output_files, self.output_values + + # Validate required files before running. This may raise; do it + # before updating the fingerprint baseline so that a failed + # validation leaves the previous baseline intact. self._validate_input_files(proj) + # Record input-file fingerprints after staleness check + validation + # but before execution, so even interrupted runs have a baseline. + set_input_fingerprints(proj, self._compute_input_fingerprints()) + # Run the workflow -- pass project_dir explicitly instead of # relying on os.chdir(). update_status(proj, WorkflowStatus.RUNNING) @@ -909,10 +1032,19 @@ async def async_launch(self): result_fields[f"result_{k}"] = v update_status(proj, WorkflowStatus.COMPLETED, **result_fields) # -- Post-completion cleanup -- - try: - self.workflow._cleanup_files() - except Exception as e: - logger.warning(f"[{self.label}] Cleanup failed (non-fatal): {e}") + # Only run cleanup when the workflow is *truly* done, i.e. + # ``can_resume_after_completed`` says no further runs would + # help. Otherwise restart.h5 / opt-step checkpoints would + # be deleted while still being needed for the next resume + # (e.g. when max_continuation was raised after exhausting + # the original budget without meeting target_error). + if not self.workflow.can_resume_after_completed(proj): + try: + self.workflow._cleanup_files() + except Exception as e: + logger.warning(f"[{self.label}] Cleanup failed (non-fatal): {e}") + else: + logger.info(f"[{self.label}] Skipping cleanup: workflow may still be resumed (target not yet met).") else: logger.error(error_msg) self.status = WorkflowStatus.FAILED @@ -926,19 +1058,32 @@ async def async_launch(self): return self.status, self.output_files, self.output_values + # Files emitted by the workflow plumbing itself (state, generated + # input TOMLs, submit scripts, scheduler stdout/stderr, accounting). + # They are recorded in [[jobs]] / [workflow] and are not meaningful + # downstream artefacts, so we exclude them from output_files. + _INTERNAL_OUTPUT_PREFIXES = ("input_", "output_", "submit_", "job_", "job_accounting_") + _INTERNAL_OUTPUT_FILES = ("workflow_state.toml", "workflow_state.toml.tmp") + def _collect_outputs(self): """Re-collect output info from state file (for already-completed runs).""" state = read_state(self.project_dir) self.output_values = state.get("result", {}) - # Gather all files in project dir as potential outputs if os.path.isdir(self.project_dir): self.output_files = [ f - for f in os.listdir(self.project_dir) - if os.path.isfile(os.path.join(self.project_dir, f)) and f != "workflow_state.toml" + for f in sorted(os.listdir(self.project_dir)) + if os.path.isfile(os.path.join(self.project_dir, f)) + and f not in self._INTERNAL_OUTPUT_FILES + and not f.startswith(self._INTERNAL_OUTPUT_PREFIXES) ] def launch(self): + """Synchronous entry point: ``asyncio.run(self.async_launch())``. + + Not callable from an already-running event loop (e.g. Jupyter); + use ``await self.async_launch()`` there, or install ``nest_asyncio``. + """ return asyncio.run(self.async_launch()) # -- Phased execution (delegates to inner Workflow) ------------ @@ -980,6 +1125,8 @@ async def async_poll(self) -> dict: if not self._bg_task.done(): summary = get_workflow_summary(self.project_dir) if self.project_dir else {} return {"status": "running", **summary} + if self._bg_task.cancelled(): + return {"status": "cancelled"} if self._bg_task.exception() is not None: return {"status": "failed", "error": str(self._bg_task.exception())} return {"status": "completed"} @@ -994,7 +1141,7 @@ async def async_collect(self) -> dict: Raises: RuntimeError: - If not submitted or still running. + If not submitted, cancelled, or still running. Exception: Re-raises the original exception if the workflow failed. """ @@ -1002,6 +1149,8 @@ async def async_collect(self) -> dict: raise RuntimeError(f"[{self.label}] Not submitted. Call async_submit() first.") if not self._bg_task.done(): raise RuntimeError(f"[{self.label}] Still running. Call async_poll() to check.") + if self._bg_task.cancelled(): + raise RuntimeError(f"[{self.label}] Workflow was cancelled before completion.") exc = self._bg_task.exception() if exc is not None: raise exc diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index 3bf5f890..41af99e9 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -2354,9 +2354,10 @@ def test_trivial_2x2(self): S_matrix = np.array([[1.0]]) K_matrix = np.array([[-0.5]]) B_matrix = np.array([[-0.1]]) - c_vec, E_lm = MCMC.solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon=1e-10) + c_vec, E_lm, v0_sq = MCMC.solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon=1e-10) assert c_vec.shape == (1,) assert E_lm <= H_0 + 1e-10, f"E_lm={E_lm} should be <= H_0={H_0}" + assert 0.0 <= v0_sq <= 1.0 + 1e-10 def test_diagonal_known_solution(self): """Diagonal H, S: verify c_vec has correct shape and E_lm is valid.""" @@ -2366,10 +2367,11 @@ def test_diagonal_known_solution(self): S_matrix = np.diag(np.linspace(0.1, 1.0, p)) K_matrix = np.diag(np.linspace(-1.0, -0.1, p)) B_matrix = np.diag(np.linspace(-0.5, -0.05, p)) - c_vec, E_lm = MCMC.solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon=1e-10) + c_vec, E_lm, v0_sq = MCMC.solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon=1e-10) assert c_vec.shape == (p,) assert np.all(np.isfinite(c_vec)) assert np.isfinite(E_lm) + assert 0.0 <= v0_sq <= 1.0 + 1e-10 def test_epsilon_cutoff(self): """S eigenvalues below epsilon are cut; p' < p.""" @@ -2379,9 +2381,10 @@ def test_epsilon_cutoff(self): S_matrix = np.diag([1.0, 0.5, 1e-8, 1e-10]) K_matrix = np.eye(p) * (-0.5) B_matrix = np.eye(p) * (-0.1) - c_vec, E_lm = MCMC.solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon=1e-6) + c_vec, E_lm, v0_sq = MCMC.solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon=1e-6) assert c_vec.shape == (p,) assert np.isfinite(E_lm) + assert 0.0 <= v0_sq <= 1.0 + 1e-10 def test_all_diag_S_zero(self): """All diag(S) = 0 -> dgelscut removes all parameters -> zero update, E_lm == H_0.""" @@ -2391,9 +2394,10 @@ def test_all_diag_S_zero(self): S_matrix = np.zeros((p, p)) K_matrix = np.eye(p) * (-0.5) B_matrix = np.eye(p) * (-0.1) - c_vec, E_lm = MCMC.solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon=1e-6) + c_vec, E_lm, v0_sq = MCMC.solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon=1e-6) np.testing.assert_array_equal(c_vec, np.zeros(p)) assert E_lm == H_0 + assert v0_sq == 0.0 def test_v0_max_selection(self): """The eigenvector with max |v_0|^2 is selected.""" @@ -2404,9 +2408,10 @@ def test_v0_max_selection(self): S_matrix = np.eye(p) K_matrix = np.diag([-10.0, -0.1]) B_matrix = np.zeros((p, p)) - c_vec, E_lm = MCMC.solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon=1e-10) + c_vec, E_lm, v0_sq = MCMC.solve_linear_method(H_0, f_vec, S_matrix, K_matrix, B_matrix, epsilon=1e-10) assert c_vec.shape == (p,) assert np.isfinite(E_lm) + assert 0.0 <= v0_sq <= 1.0 + 1e-10 # --------------------------------------------------------------------------- @@ -2618,10 +2623,11 @@ def test_get_aH_and_solve_lm_debug_vs_production(): # Use the production matrices for both to verify the two implementations # produce identical results when given the exact same input. epsilon_lm = 1e-6 - c_debug, E_debug = _MCMC_debug.solve_linear_method(H_0_p, f_p, S_p, K_p, B_p, epsilon_lm) - c_prod, E_prod = MCMC.solve_linear_method(H_0_p, f_p, S_p, K_p, B_p, epsilon_lm) + c_debug, E_debug, v0_debug = _MCMC_debug.solve_linear_method(H_0_p, f_p, S_p, K_p, B_p, epsilon_lm) + c_prod, E_prod, v0_prod = MCMC.solve_linear_method(H_0_p, f_p, S_p, K_p, B_p, epsilon_lm) np.testing.assert_allclose(c_debug, c_prod, atol=atol, rtol=rtol) np.testing.assert_allclose(E_debug, E_prod, atol=atol, rtol=rtol) + np.testing.assert_allclose(v0_debug, v0_prod, atol=atol, rtol=rtol) jax.clear_caches() From b6d50aae3c9f3c3a2618854d636034acaafe0ea9 Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Tue, 26 May 2026 16:33:31 +0900 Subject: [PATCH 77/97] Fix a trivial bug in GFMC_t. add force-grad compile time to GFMC_t `timer_projection_init`. It was previously leaked into Net GFMC time when `comput_position_deriv=True`. --- jqmc/jqmc_gfmc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 1260133f..3a64169f 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -1543,6 +1543,7 @@ def _compute_local_energy_t( _ = _jit_vmap_swct_domega_t(self.__hamiltonian_data.structure_data, self.__latest_r_up_carts) _ = _jit_vmap_swct_domega_t(self.__hamiltonian_data.structure_data, self.__latest_r_dn_carts) end_init_force = time.perf_counter() + timer_projection_init += end_init_force - start_init_force logger.info("End compilation of force gradient functions.") logger.info(f"Elapsed Time = {end_init_force - start_init_force:.2f} sec.") logger.info("") From fcfce2fc29059f9a05842e9a177ebaff2961e4ab Mon Sep 17 00:00:00 2001 From: kousuke Date: Tue, 26 May 2026 18:00:56 +0900 Subject: [PATCH 78/97] Block A_inv before Barrier in GFMC_n/GFMC_t to keep dispatch-queue skew out of barrier wait --- jqmc/jqmc_gfmc.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index 3a64169f..e6a8e6a2 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -2295,6 +2295,9 @@ def _run_projection_loop_streaming(pcl, tll, wll, ru, rd, Ainv, key, ks): self.__latest_A_old_inv = vmap(_compute_initial_A_inv_t, in_axes=(0, 0))( self.__latest_r_up_carts, self.__latest_r_dn_carts ) + # block before Barrier so the A_inv GPU work is included in timer_reconfiguration + # and dispatch-queue skew does not leak into the next step's barrier wait. + self.__latest_A_old_inv.block_until_ready() # Barrier after MPI operation mpi_comm.Barrier() @@ -6365,6 +6368,9 @@ def _compute_local_energy_n( self.__latest_r_up_carts = jnp.asarray(latest_r_up_carts_after_branching, dtype=jnp.float64) self.__latest_r_dn_carts = jnp.asarray(latest_r_dn_carts_after_branching, dtype=jnp.float64) self.__latest_A_old_inv = _jit_vmap_A_inv_n(self.__latest_r_up_carts, self.__latest_r_dn_carts) + # block before Barrier so the A_inv GPU work is included in timer_reconfiguration + # and dispatch-queue skew does not leak into the next step's barrier wait. + self.__latest_A_old_inv.block_until_ready() mpi_comm.Barrier() From c930eefe100c4dae36e6bf338b770abc3009f4a4 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Tue, 26 May 2026 22:57:23 +0900 Subject: [PATCH 79/97] MCMC/GFMC: trivial fix: barrier compile/total boundaries to keep rank imbalance out of Net time --- jqmc/jqmc_gfmc.py | 4 ++++ jqmc/jqmc_mcmc.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/jqmc/jqmc_gfmc.py b/jqmc/jqmc_gfmc.py index e6a8e6a2..43cf0c6b 100644 --- a/jqmc/jqmc_gfmc.py +++ b/jqmc/jqmc_gfmc.py @@ -659,6 +659,7 @@ def run(self, num_mcmc_steps: int = 50, max_time: int = 86400) -> None: timer_mpi_barrier = 0.0 timer_collection = 0.0 timer_reconfiguration = 0.0 + mpi_comm.Barrier() gfmc_total_start = time.perf_counter() # toml(control) filename @@ -1290,6 +1291,7 @@ def _projection_t_streaming( self.__alat, self.__hamiltonian_data, ) + mpi_comm.Barrier() end_init = time.perf_counter() timer_projection_init += end_init - start_init logger.info("End compilation of the GFMC projection funciton.") @@ -1677,6 +1679,7 @@ def _run_projection_loop_streaming(pcl, tll, wll, ru, rd, Ainv, key, ks): self.__jax_PRNG_key_list, _init_kinetic_state_list_compile, ).compile() + mpi_comm.Barrier() end_warmup = time.perf_counter() timer_projection_init += end_warmup - start_warmup logger.info("End compilation of the GFMC projection while_loop driver.") @@ -5786,6 +5789,7 @@ def _compute_local_energy_n( _ = _jit_vmap_swct_omega_n(self.__hamiltonian_data.structure_data, self.__latest_r_dn_carts) _ = _jit_vmap_swct_domega_n(self.__hamiltonian_data.structure_data, self.__latest_r_up_carts) _ = _jit_vmap_swct_domega_n(self.__hamiltonian_data.structure_data, self.__latest_r_dn_carts) + mpi_comm.Barrier() end_init = time.perf_counter() timer_projection_init += end_init - start_init logger.info("End compilation of the GFMC projection funciton.") diff --git a/jqmc/jqmc_mcmc.py b/jqmc/jqmc_mcmc.py index 785f1935..cb1ecaa5 100644 --- a/jqmc/jqmc_mcmc.py +++ b/jqmc/jqmc_mcmc.py @@ -784,6 +784,7 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: timer_MPI_barrier = 0.0 # mcmc timer starts + mpi_comm.Barrier() mcmc_total_start = time.perf_counter() # toml(control) filename @@ -961,12 +962,14 @@ def run(self, num_mcmc_steps: int = 0, max_time=86400) -> None: ) self.__mcmc_kernels_warmed_up = True + mpi_comm.Barrier() mcmc_update_init_end = time.perf_counter() timer_mcmc_update_init += mcmc_update_init_end - mcmc_update_init_start logger.info("End compilation of the MCMC_update funciton.") logger.info(f"Elapsed Time = {mcmc_update_init_end - mcmc_update_init_start:.2f} sec.") logger.info("") else: + mpi_comm.Barrier() logger.info("Skipping compilation (JAX cache is warm from previous run).") logger.info("") From 986481248b8f5831d94ccaa2a6474f45ed612ff1 Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Wed, 27 May 2026 10:44:58 +0900 Subject: [PATCH 80/97] Add an explicit click dependency in setup.cfg. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 375fea96..0dfc15a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,6 +37,7 @@ install_requires = pyyaml >= 6.0.0 toml >= 0.10.2 typer >= 0.15.1 + click >= 8.0.0 tomlkit >= 0.13.2 uncertainties >= 3.2.2 matplotlib >= 3.10.1 From d6392af962d7eae3812731c66978f8045a001eeb Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Wed, 27 May 2026 10:46:30 +0900 Subject: [PATCH 81/97] shorten jqmc-run-long-pytest.yml. --- .github/workflows/jqmc-run-long-pytest.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/jqmc-run-long-pytest.yml b/.github/workflows/jqmc-run-long-pytest.yml index 99248a8a..dd0265a1 100644 --- a/.github/workflows/jqmc-run-long-pytest.yml +++ b/.github/workflows/jqmc-run-long-pytest.yml @@ -44,7 +44,7 @@ jobs: - name: Install jqmc run: | - python -m pip install flake8 pytest pytest-cov + python -m pip install pytest pytest-cov python -m pip install . - name: Test jqmc FP64/FP32+FP64 (Intra-software comparisons) @@ -76,13 +76,11 @@ jobs: pytest -s -v tests/test_comparison_with_turborvb_ECP.py pytest -s -v tests/test_comparison_with_turborvb_AE.py - - name: Test jqmc FP64/FP32+FP64 (QMC kernels without MPI) + - name: Test jqmc FP64 (QMC kernels without MPI) run: | pytest -s -v tests/test_jqmc_command_lines.py pytest -s -v tests/test_jqmc_mcmc.py - pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed pytest -s -v tests/test_jqmc_gfmc_tau.py - pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - name: Test jqmc-tool (toolset for jqmc) run: | From 776882a65e887cd1cddaa7979388e46cef06d344 Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Wed, 27 May 2026 13:58:19 +0900 Subject: [PATCH 82/97] Cleanup GitHub workflows. --- .github/workflows/jqmc-deploy-gh-pages.yml | 6 +- .github/workflows/jqmc-deploy-test.yml | 4 +- .github/workflows/jqmc-deploy.yml | 4 +- .github/workflows/jqmc-lint-ruff.yml | 3 +- .github/workflows/jqmc-run-full-pytest.yml | 78 ++++++++++++------- .github/workflows/jqmc-run-long-pytest.yml | 3 +- .../jqmc-run-rc-full-precision-pytest.yml | 73 ----------------- .../jqmc-run-rc-mixed-precision-pytest.yml | 68 ---------------- .github/workflows/jqmc-run-short-pytest.yml | 3 +- 9 files changed, 65 insertions(+), 177 deletions(-) delete mode 100644 .github/workflows/jqmc-run-rc-full-precision-pytest.yml delete mode 100644 .github/workflows/jqmc-run-rc-mixed-precision-pytest.yml diff --git a/.github/workflows/jqmc-deploy-gh-pages.yml b/.github/workflows/jqmc-deploy-gh-pages.yml index 9d7db4ca..38080097 100644 --- a/.github/workflows/jqmc-deploy-gh-pages.yml +++ b/.github/workflows/jqmc-deploy-gh-pages.yml @@ -1,4 +1,4 @@ -name: publish jqmc gh-pages +name: publish jqmc gh-pages workflow on: push: @@ -9,6 +9,10 @@ permissions: jobs: docs: + name: publish jqmc gh-pages + + if: github.repository == 'jqmc-project/jQMC' + runs-on: ubuntu-latest defaults: run: diff --git a/.github/workflows/jqmc-deploy-test.yml b/.github/workflows/jqmc-deploy-test.yml index e267c69e..929a6a70 100644 --- a/.github/workflows/jqmc-deploy-test.yml +++ b/.github/workflows/jqmc-deploy-test.yml @@ -1,10 +1,12 @@ -name: Publish jQMC distributions to test-PyPI +name: publish jQMC to test-PyPI workflow on: push: branches: [ "rc" ] jobs: deploy-test-pypi: + name: publish jqmc to test-pypi + if: github.repository == 'jqmc-project/jQMC' runs-on: ubuntu-latest diff --git a/.github/workflows/jqmc-deploy.yml b/.github/workflows/jqmc-deploy.yml index c9eafe8f..af559863 100644 --- a/.github/workflows/jqmc-deploy.yml +++ b/.github/workflows/jqmc-deploy.yml @@ -1,4 +1,4 @@ -name: Publish jQMC distributions to PyPI +name: Publish jQMC to PyPI workflow # Trigger only when tags that start with "v" are pushed (e.g., v0.1.0) on: @@ -9,6 +9,7 @@ on: jobs: # validate tag validate_tag: + name: validate jqmc tag # Run only if this repository is rc if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'jqmc-project/jQMC' runs-on: ubuntu-latest @@ -81,6 +82,7 @@ jobs: # deploy deploy-pypi: + name: publish jqmc to pypi # Run only if this repository is rc needs: validate_tag if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'jqmc-project/jQMC' diff --git a/.github/workflows/jqmc-lint-ruff.yml b/.github/workflows/jqmc-lint-ruff.yml index 8c65f6f2..db6c2252 100644 --- a/.github/workflows/jqmc-lint-ruff.yml +++ b/.github/workflows/jqmc-lint-ruff.yml @@ -7,7 +7,7 @@ # To enforce additional ruff rules, fix the violations listed in # `lint.extend-ignore` in pyproject.toml and remove them from that list. -name: jqmc lint (ruff + pre-commit) +name: jqmc lint (ruff + pre-commit) workflow on: push: @@ -17,6 +17,7 @@ on: jobs: lint: + name: jqmc lint (ruff + pre-commit) runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index 2744aa70..6241aee0 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -1,50 +1,50 @@ -# A full test of jqmc. +# A full manual test of jqmc on a self-hosted runner. -name: jqmc full test +name: jqmc full test workflow on: - push: - branches: [ "main" ] - paths-ignore: - - '.gitignore' - - '.github/**' - - 'doc/**' - - 'examples/**' - - 'benchmarks/**' - - 'README.md' - - '.pre-commit-config.yaml' - - 'jqmc_workflow/**' + workflow_dispatch: + +permissions: + contents: read jobs: run: - runs-on: ubuntu-latest + name: jqmc full test + Codecov / Python ${{ matrix.python-version }} + + if: github.repository == 'jqmc-project/jQMC' + + runs-on: + group: jqmc-nightly-runners + labels: [self-hosted, Linux, X64] + strategy: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12"] + timeout-minutes: 720 + steps: - - name: Install gfortran and gcc + - name: Show runner information run: | - sudo apt-get update - sudo apt-get install gfortran - - - name: Install OpenBLAS and LAPACK - run: sudo apt-get install libopenblas-dev liblapack-dev - - - name: Install OpenMPI - run: sudo apt-get install openmpi-bin libopenmpi-dev + hostname + uname -a + cat /etc/os-release + gcc --version + gfortran --version - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install jqmc run: | - python -m pip install flake8 pytest pytest-cov + python -m pip install -U pip + python -m pip install pytest pytest-cov python -m pip install . - name: Test jqmc FP64 (Intra-software comparisons) @@ -66,7 +66,7 @@ jobs: pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append # Skipped under full mode: - # pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP32+FP64 (Intra-software comparisons) run: | @@ -88,27 +88,45 @@ jobs: # pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed # pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - name: Test jqmc FP64 (Inter-software comparisons) run: | pytest -s -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - name: Test jqmc FP64 (QMC kernels without MPI) + - name: Test jqmc FP64 (QMC kernels without MPI, FP64) run: | pytest -s -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append pytest -s -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) + run: | + pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed + pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + + - name: Test jqmc FP64 (QMC kernels with 2MPIs, FP64) + run: | + mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py + mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py + mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py + + - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) + run: | + mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed + mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + - name: Test jqmc-tool (Toolset for jqmc) run: | pytest -s -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Combine Codecov xml files run: | - coverage xml -o coverage.xml + python -m coverage xml -o coverage.xml - name: Upload coverage reports to Codecov if: matrix.python-version == '3.12' diff --git a/.github/workflows/jqmc-run-long-pytest.yml b/.github/workflows/jqmc-run-long-pytest.yml index 99248a8a..cadae05d 100644 --- a/.github/workflows/jqmc-run-long-pytest.yml +++ b/.github/workflows/jqmc-run-long-pytest.yml @@ -1,6 +1,6 @@ # A long test of jqmc. -name: jqmc long test +name: jqmc long test workflow on: pull_request: @@ -17,6 +17,7 @@ on: jobs: run: + name: jqmc long test / Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/jqmc-run-rc-full-precision-pytest.yml b/.github/workflows/jqmc-run-rc-full-precision-pytest.yml deleted file mode 100644 index 07e43316..00000000 --- a/.github/workflows/jqmc-run-rc-full-precision-pytest.yml +++ /dev/null @@ -1,73 +0,0 @@ -# An rc test of jqmc. - -name: jqmc rc test - -on: - pull_request: - branches: [ "rc" ] - paths-ignore: - - '.gitignore' - - '.github/**' - - 'doc/**' - - 'examples/**' - - 'benchmarks/**' - - 'README.md' - - '.pre-commit-config.yaml' - - 'jqmc_workflow/**' - -jobs: - run: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.10", "3.11", "3.12"] - - steps: - - name: Install gfortran and gcc - run: | - sudo apt-get update - sudo apt-get install gfortran - - - name: Install OpenBLAS and LAPACK - run: sudo apt-get install libopenblas-dev liblapack-dev - - - name: Install OpenMPI - run: sudo apt-get install openmpi-bin libopenmpi-dev - - - uses: actions/checkout@v3 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - - name: Install jqmc - run: | - python -m pip install flake8 pytest pytest-cov - python -m pip install . - - - name: Test jqmc command-line - run: | - pytest -s -v tests/test_jqmc_command_lines.py - - - name: Test jqmc FP64 (Inter-software comparisons) - run: | - pytest -s -v tests/test_comparison_with_turborvb_ECP.py - pytest -s -v tests/test_comparison_with_turborvb_AE.py - - - name: Test jqmc FP64 (QMC kernels without MPI, FP64) - run: | - pytest -s -v tests/test_jqmc_mcmc.py - pytest -s -v tests/test_jqmc_gfmc_tau.py - pytest -s -v tests/test_jqmc_gfmc_bra.py - - - name: Test jqmc FP64 (QMC kernels with 2MPIs, FP64) - run: | - mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py - mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py - mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py - - - name: Test jqmc-tool (toolset for jqmc) - run: | - pytest -s -v tests/test_jqmc_tool.py diff --git a/.github/workflows/jqmc-run-rc-mixed-precision-pytest.yml b/.github/workflows/jqmc-run-rc-mixed-precision-pytest.yml deleted file mode 100644 index c45a2974..00000000 --- a/.github/workflows/jqmc-run-rc-mixed-precision-pytest.yml +++ /dev/null @@ -1,68 +0,0 @@ -# An rc test of jqmc. - -name: jqmc rc test - -on: - pull_request: - branches: [ "rc" ] - paths-ignore: - - '.gitignore' - - '.github/**' - - 'doc/**' - - 'examples/**' - - 'benchmarks/**' - - 'README.md' - - '.pre-commit-config.yaml' - - 'jqmc_workflow/**' - -jobs: - run: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.10", "3.11", "3.12"] - - steps: - - name: Install gfortran and gcc - run: | - sudo apt-get update - sudo apt-get install gfortran - - - name: Install OpenBLAS and LAPACK - run: sudo apt-get install libopenblas-dev liblapack-dev - - - name: Install OpenMPI - run: sudo apt-get install openmpi-bin libopenmpi-dev - - - uses: actions/checkout@v3 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - - name: Install jqmc - run: | - python -m pip install flake8 pytest pytest-cov - python -m pip install . - - - name: Test jqmc command-line - run: | - pytest -s -v tests/test_jqmc_command_lines.py - - - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) - run: | - pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed - pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed - pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - - - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) - run: | - mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed - mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed - mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - - - name: Test jqmc-tool (toolset for jqmc) - run: | - pytest -s -v tests/test_jqmc_tool.py diff --git a/.github/workflows/jqmc-run-short-pytest.yml b/.github/workflows/jqmc-run-short-pytest.yml index 55877b9d..9cd14c54 100644 --- a/.github/workflows/jqmc-run-short-pytest.yml +++ b/.github/workflows/jqmc-run-short-pytest.yml @@ -1,6 +1,6 @@ # A short test jqmc. -name: jqmc short test +name: jqmc short test workflow on: push: @@ -28,6 +28,7 @@ on: jobs: run: + name: jqmc short test / Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: fail-fast: false From b3d9e08a7661c2f34c4531362ccc2f82774685c5 Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Wed, 27 May 2026 14:00:26 +0900 Subject: [PATCH 83/97] update --- .github/workflows/jqmc-deploy-gh-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jqmc-deploy-gh-pages.yml b/.github/workflows/jqmc-deploy-gh-pages.yml index 38080097..749f22b0 100644 --- a/.github/workflows/jqmc-deploy-gh-pages.yml +++ b/.github/workflows/jqmc-deploy-gh-pages.yml @@ -1,4 +1,4 @@ -name: publish jqmc gh-pages workflow +name: Publish jqmc gh-pages workflow on: push: From 252c09716c7407c78603c8e8702facdad1abf7af Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Wed, 27 May 2026 14:28:32 +0900 Subject: [PATCH 84/97] test nightly CI --- .github/workflows/jqmc-run-full-pytest.yml | 97 +++++++++++----------- 1 file changed, 47 insertions(+), 50 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index 6241aee0..486c9934 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -50,87 +50,84 @@ jobs: - name: Test jqmc FP64 (Intra-software comparisons) run: | pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail - pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # Skipped under full mode: + # pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append # pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP32+FP64 (Intra-software comparisons) run: | - # Skipped under mixed mode: precision-insensitive (coverage already obtained in FP64 block). - # pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed # pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed # pytest -s -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # Skipped under mixed mode: HDF5 roundtrip tests, precision-insensitive (coverage already obtained in FP64 block). + # pytest -s -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed # pytest -s -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed # pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed # pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - name: Test jqmc FP64 (Inter-software comparisons) run: | pytest -s -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP64 (QMC kernels without MPI, FP64) run: | pytest -s -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -s -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) run: | - pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed - pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed - pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP64 (QMC kernels with 2MPIs, FP64) run: | mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py - mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py - mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py + # mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py + # mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) run: | mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed - mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed - mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + # mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + # mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - name: Test jqmc-tool (Toolset for jqmc) run: | pytest -s -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - name: Combine Codecov xml files - run: | - python -m coverage xml -o coverage.xml + # - name: Combine Codecov xml files + # run: | + # python -m coverage xml -o coverage.xml - - name: Upload coverage reports to Codecov - if: matrix.python-version == '3.12' - uses: codecov/codecov-action@v5 - with: - files: coverage.xml - token: ${{ secrets.CODECOV_TOKEN }} + # - name: Upload coverage reports to Codecov + # if: matrix.python-version == '3.12' + # uses: codecov/codecov-action@v5 + # with: + # files: coverage.xml + # token: ${{ secrets.CODECOV_TOKEN }} From f8274d7833d0bf78821ed0b1f5b9bcf72aa5d154 Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Wed, 27 May 2026 14:53:30 +0900 Subject: [PATCH 85/97] test nightly CI --- .github/workflows/jqmc-run-full-pytest.yml | 24 ++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index 486c9934..00d1df9e 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.10.14", "3.11.9", "3.12.3"] timeout-minutes: 720 @@ -31,19 +31,27 @@ jobs: hostname uname -a cat /etc/os-release - gcc --version - gfortran --version - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} + - name: Select Python + run: | + PY="$HOME/.pyenv/versions/${{ matrix.python-version }}/bin" + echo "$PY" >> "$GITHUB_PATH" + "$PY/python" --version + + - name: Setup Open MPI + run: | + source /etc/profile.d/modules.sh + module load mpi/openmpi-x86_64 + echo "$(dirname "$(which mpirun)")" >> "$GITHUB_PATH" + echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" >> "$GITHUB_ENV" + mpirun --version + mpicc --version - name: Install jqmc run: | - python -m pip install -U pip + python -m pip install --upgrade pip python -m pip install pytest pytest-cov python -m pip install . From 18e3cb6cf47ae6eab7cc78b407852739066e0849 Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Wed, 27 May 2026 14:55:50 +0900 Subject: [PATCH 86/97] test nightly CI --- .github/workflows/jqmc-run-full-pytest.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml index 00d1df9e..a72fb105 100644 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ b/.github/workflows/jqmc-run-full-pytest.yml @@ -35,19 +35,19 @@ jobs: - uses: actions/checkout@v4 - name: Select Python - run: | - PY="$HOME/.pyenv/versions/${{ matrix.python-version }}/bin" - echo "$PY" >> "$GITHUB_PATH" - "$PY/python" --version + run: | + PY="$HOME/.pyenv/versions/${{ matrix.python-version }}/bin" + echo "$PY" >> "$GITHUB_PATH" + "$PY/python" --version - name: Setup Open MPI - run: | - source /etc/profile.d/modules.sh - module load mpi/openmpi-x86_64 - echo "$(dirname "$(which mpirun)")" >> "$GITHUB_PATH" - echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" >> "$GITHUB_ENV" - mpirun --version - mpicc --version + run: | + source /etc/profile.d/modules.sh + module load mpi/openmpi-x86_64 + echo "$(dirname "$(which mpirun)")" >> "$GITHUB_PATH" + echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" >> "$GITHUB_ENV" + mpirun --version + mpicc --version - name: Install jqmc run: | From 201a48d5c3e02c0d3c31c592061eb9745bcd5d35 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 28 May 2026 11:30:14 +0900 Subject: [PATCH 87/97] support pytest-xdist in the nightly CI. --- .../workflows/jqmc-run-full-pytest-ubuntu.yml | 135 +++++++++++++++++ .github/workflows/jqmc-run-full-pytest.yml | 141 ------------------ 2 files changed, 135 insertions(+), 141 deletions(-) create mode 100644 .github/workflows/jqmc-run-full-pytest-ubuntu.yml delete mode 100644 .github/workflows/jqmc-run-full-pytest.yml diff --git a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml new file mode 100644 index 00000000..6e32128a --- /dev/null +++ b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml @@ -0,0 +1,135 @@ +# A full manual test of jqmc on a self-hosted runner. + +name: jqmc full test workflow (ubuntu) + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + run: + name: jqmc full test + Codecov / Python ${{ matrix.python-version }} + + if: github.repository == 'jqmc-project/jQMC' + + runs-on: + group: jqmc-nightly-runners-ubuntu + labels: [self-hosted, Linux, X64] + + strategy: + fail-fast: false + matrix: + python-version: ["3.10.14", "3.11.9", "3.12.3"] + + timeout-minutes: 720 + + steps: + - name: Show runner information + run: | + hostname + uname -a + cat /etc/os-release + + - uses: actions/checkout@v4 + + - name: Select Python + run: | + PY="$HOME/.pyenv/versions/${{ matrix.python-version }}/bin" + echo "$PY" >> "$GITHUB_PATH" + "$PY/python" --version + + - name: Install pytest, pytest-xdist, pytest-cov + run: | + python -m pip install --upgrade pip + python -m pip install pytest, pytest-xdist, pytest-cov + + - name: Install jqmc + run: | + python -m pip install . + + - name: Test jqmc FP64 (Intra-software comparisons) + run: | + pytest -n 8 -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail + # pytest -n 8 -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + + - name: Test jqmc FP32+FP64 (Intra-software comparisons) + run: | + pytest -n 8 -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 8 -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + + - name: Test jqmc FP64 (Inter-software comparisons) + run: | + pytest -n 8 -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + + - name: Test jqmc FP64 (QMC kernels without MPI, FP64) + run: | + pytest -n 8 -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + + - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) + run: | + pytest -n 8 -v tests/test_jqmc_mcmc.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 8 -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + + - name: Test jqmc FP64 (QMC kernels with 2MPIs, FP64) + run: | + mpirun -np 4 pytest -v tests/test_jqmc_mcmc.py + # mpirun -np 4 pytest -v tests/test_jqmc_gfmc_tau.py + # mpirun -np 4 pytest -v tests/test_jqmc_gfmc_bra.py + + - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) + run: | + mpirun -np 4 pytest -v tests/test_jqmc_mcmc.py --precision-mode=mixed + # mpirun -np 4 pytest -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + # mpirun -np 4 pytest -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + + - name: Test jqmc-tool (Toolset for jqmc) + run: | + pytest -n 8 -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + + # - name: Combine Codecov xml files + # run: | + # python -m coverage xml -o coverage.xml + + # - name: Upload coverage reports to Codecov + # if: matrix.python-version == '3.12' + # uses: codecov/codecov-action@v5 + # with: + # files: coverage.xml + # token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/jqmc-run-full-pytest.yml b/.github/workflows/jqmc-run-full-pytest.yml deleted file mode 100644 index a72fb105..00000000 --- a/.github/workflows/jqmc-run-full-pytest.yml +++ /dev/null @@ -1,141 +0,0 @@ -# A full manual test of jqmc on a self-hosted runner. - -name: jqmc full test workflow - -on: - workflow_dispatch: - -permissions: - contents: read - -jobs: - run: - name: jqmc full test + Codecov / Python ${{ matrix.python-version }} - - if: github.repository == 'jqmc-project/jQMC' - - runs-on: - group: jqmc-nightly-runners - labels: [self-hosted, Linux, X64] - - strategy: - fail-fast: false - matrix: - python-version: ["3.10.14", "3.11.9", "3.12.3"] - - timeout-minutes: 720 - - steps: - - name: Show runner information - run: | - hostname - uname -a - cat /etc/os-release - - - uses: actions/checkout@v4 - - - name: Select Python - run: | - PY="$HOME/.pyenv/versions/${{ matrix.python-version }}/bin" - echo "$PY" >> "$GITHUB_PATH" - "$PY/python" --version - - - name: Setup Open MPI - run: | - source /etc/profile.d/modules.sh - module load mpi/openmpi-x86_64 - echo "$(dirname "$(which mpirun)")" >> "$GITHUB_PATH" - echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" >> "$GITHUB_ENV" - mpirun --version - mpicc --version - - - name: Install jqmc - run: | - python -m pip install --upgrade pip - python -m pip install pytest pytest-cov - python -m pip install . - - - name: Test jqmc FP64 (Intra-software comparisons) - run: | - pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail - # pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - - name: Test jqmc FP32+FP64 (Intra-software comparisons) - run: | - pytest -s -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -s -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - - - name: Test jqmc FP64 (Inter-software comparisons) - run: | - pytest -s -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - - name: Test jqmc FP64 (QMC kernels without MPI, FP64) - run: | - pytest -s -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) - run: | - pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - - name: Test jqmc FP64 (QMC kernels with 2MPIs, FP64) - run: | - mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py - # mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py - # mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py - - - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) - run: | - mpirun -np 2 pytest -s -v tests/test_jqmc_mcmc.py --precision-mode=mixed - # mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed - # mpirun -np 2 pytest -s -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - - - name: Test jqmc-tool (Toolset for jqmc) - run: | - pytest -s -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - # - name: Combine Codecov xml files - # run: | - # python -m coverage xml -o coverage.xml - - # - name: Upload coverage reports to Codecov - # if: matrix.python-version == '3.12' - # uses: codecov/codecov-action@v5 - # with: - # files: coverage.xml - # token: ${{ secrets.CODECOV_TOKEN }} From e4a6e6fd2d7da604d00bdd08ff698e7446907815 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 28 May 2026 13:03:46 +0900 Subject: [PATCH 88/97] fix a typo. --- .github/workflows/jqmc-run-full-pytest-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml index 6e32128a..c8b51b90 100644 --- a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml +++ b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml @@ -43,7 +43,7 @@ jobs: - name: Install pytest, pytest-xdist, pytest-cov run: | python -m pip install --upgrade pip - python -m pip install pytest, pytest-xdist, pytest-cov + python -m pip install pytest pytest-xdist pytest-cov - name: Install jqmc run: | From be77e2ae093e7852df83881cf15703dd4892ea03 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 28 May 2026 13:54:39 +0900 Subject: [PATCH 89/97] Solve the name conflict in tests/ with pytest-xdist --- .../workflows/jqmc-run-full-pytest-ubuntu.yml | 20 +++++++++---------- tests/conftest.py | 14 +++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml index c8b51b90..6142777a 100644 --- a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml +++ b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml @@ -96,16 +96,16 @@ jobs: - name: Test jqmc FP64 (QMC kernels without MPI, FP64) run: | - pytest -n 8 -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 4 -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) run: | - pytest -n 8 -v tests/test_jqmc_mcmc.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 4 -v tests/test_jqmc_mcmc.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP64 (QMC kernels with 2MPIs, FP64) run: | @@ -119,16 +119,16 @@ jobs: # mpirun -np 4 pytest -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed # mpirun -np 4 pytest -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - - name: Test jqmc-tool (Toolset for jqmc) + - name: Test jqmc-tool (Toolset for jqmc, FP64) run: | - pytest -n 8 -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 4 -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append # - name: Combine Codecov xml files # run: | # python -m coverage xml -o coverage.xml # - name: Upload coverage reports to Codecov - # if: matrix.python-version == '3.12' + # if: matrix.python-version == '3.12.3' # uses: codecov/codecov-action@v5 # with: # files: coverage.xml diff --git a/tests/conftest.py b/tests/conftest.py index d390b0d5..d723850b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -70,6 +70,20 @@ def configure_precision(request): configure(mode) +@pytest.fixture(autouse=True) +def _isolated_cwd(tmp_path, monkeypatch): + """Run every test in its own pytest-managed tmp directory. + + Why: production code (e.g. ``MCMC.run_optimize``) writes artifacts such + as ``hamiltonian_data_opt_step_.h5`` to the current working directory + with fixed filenames. Under pytest-xdist, all workers share one cwd, so + concurrent tests collide on h5py's exclusive file lock (EWOULDBLOCK). + Per-test cwd isolation removes the collision and stops tests/ from + accumulating stale artifacts across runs. + """ + monkeypatch.chdir(tmp_path) + + def pytest_itemcollected(item): """Show reason for obsolete tests.""" obsolete_marker = item.get_closest_marker("obsolete") From 5abf50e6ea3fa14ba7434332a64c5d8a4d1ceab0 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 28 May 2026 15:50:01 +0900 Subject: [PATCH 90/97] Replace -n 8 with -n 4 due to the limited memory of the local machine (to be upgraded). --- .../workflows/jqmc-run-full-pytest-ubuntu.yml | 99 ++++++++++--------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml index 6142777a..a02215de 100644 --- a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml +++ b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml @@ -51,73 +51,74 @@ jobs: - name: Test jqmc FP64 (Intra-software comparisons) run: | - pytest -n 8 -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail - # pytest -n 8 -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 4 -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail + # pytest -n 4 -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP32+FP64 (Intra-software comparisons) run: | - pytest -n 8 -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 8 -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 4 -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + # pytest -n 4 -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - name: Test jqmc FP64 (Inter-software comparisons) run: | - pytest -n 8 -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 8 -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 4 -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 4 -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP64 (QMC kernels without MPI, FP64) run: | - pytest -n 4 -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 2 -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 2 -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 2 -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 2 -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) run: | - pytest -n 4 -v tests/test_jqmc_mcmc.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 2 -v tests/test_jqmc_command_lines.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + # pytest -n 2 -v tests/test_jqmc_mcmc.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 2 -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 2 -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP64 (QMC kernels with 2MPIs, FP64) run: | - mpirun -np 4 pytest -v tests/test_jqmc_mcmc.py - # mpirun -np 4 pytest -v tests/test_jqmc_gfmc_tau.py - # mpirun -np 4 pytest -v tests/test_jqmc_gfmc_bra.py + mpirun -np 2 pytest -v tests/test_jqmc_mcmc.py + # mpirun -np 2 pytest -v tests/test_jqmc_gfmc_tau.py + # mpirun -np 2 pytest -v tests/test_jqmc_gfmc_bra.py - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) run: | - mpirun -np 4 pytest -v tests/test_jqmc_mcmc.py --precision-mode=mixed - # mpirun -np 4 pytest -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed - # mpirun -np 4 pytest -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + # mpirun -np 2 pytest -v tests/test_jqmc_mcmc.py --precision-mode=mixed + mpirun -np 2 pytest -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + mpirun -np 2 pytest -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - name: Test jqmc-tool (Toolset for jqmc, FP64) run: | From ee83bc3ebbc911a36a942e7dc8d736202f4f2e2c Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Thu, 28 May 2026 19:54:51 +0900 Subject: [PATCH 91/97] Shrink NN-Jastrow size in tests to reduce XLA compile memory. Set hidden_dim=2, num_layers=1, num_rbf=2 (test_jastrow: 4/1/4 for numerical-diff). --- tests/test_checkpoint_gfmc.py | 4 +++- tests/test_checkpoint_mcmc.py | 4 +++- tests/test_hamiltonian.py | 4 +++- tests/test_jastrow.py | 6 +++--- tests/test_jqmc_gfmc_bra.py | 2 +- tests/test_jqmc_gfmc_tau.py | 2 +- tests/test_jqmc_mcmc.py | 6 ++++-- tests/test_lrdmc_force.py | 4 ++-- tests/test_mcmc_force.py | 4 ++-- tests/test_wave_function.py | 4 +++- 10 files changed, 25 insertions(+), 15 deletions(-) diff --git a/tests/test_checkpoint_gfmc.py b/tests/test_checkpoint_gfmc.py index aefdfe34..daa1a799 100644 --- a/tests/test_checkpoint_gfmc.py +++ b/tests/test_checkpoint_gfmc.py @@ -85,7 +85,9 @@ def _build_hamiltonian(trexio_file, jastrow_combo): ) if jastrow_combo == "1b+2b+3b+nn": - nn_jastrow_data = Jastrow_NN_data.init_from_structure(structure_data=structure_data) + nn_jastrow_data = Jastrow_NN_data.init_from_structure( + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0 + ) jastrow_data = Jastrow_data( jastrow_one_body_data=jastrow_one_body_data, diff --git a/tests/test_checkpoint_mcmc.py b/tests/test_checkpoint_mcmc.py index da868377..527d1211 100644 --- a/tests/test_checkpoint_mcmc.py +++ b/tests/test_checkpoint_mcmc.py @@ -93,7 +93,9 @@ def _build_hamiltonian(trexio_file, jastrow_combo): ) if jastrow_combo == "1b+2b+3b+nn": - nn_jastrow_data = Jastrow_NN_data.init_from_structure(structure_data=structure_data) + nn_jastrow_data = Jastrow_NN_data.init_from_structure( + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0 + ) jastrow_data = Jastrow_data( jastrow_one_body_data=jastrow_one_body_data, diff --git a/tests/test_hamiltonian.py b/tests/test_hamiltonian.py index 1fbc92d3..640721f1 100644 --- a/tests/test_hamiltonian.py +++ b/tests/test_hamiltonian.py @@ -162,7 +162,9 @@ def test_hamiltonian_hdf5(trexio_file, use_1b, use_2b, use_3b, use_nn, geminal_t nn_jastrow_data = None if use_nn: - nn_jastrow_data = Jastrow_NN_data.init_from_structure(structure_data=structure_data) + nn_jastrow_data = Jastrow_NN_data.init_from_structure( + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0 + ) jastrow_data = Jastrow_data( jastrow_one_body_data=jastrow_one_body_data, diff --git a/tests/test_jastrow.py b/tests/test_jastrow.py index 0d6b3245..41dd1837 100755 --- a/tests/test_jastrow.py +++ b/tests/test_jastrow.py @@ -1323,9 +1323,9 @@ def _build_jastrow_data_for_part_tests(j1b_type: str = "exp", j2b_type: str = "p if include_nn: jastrow_nn_data = Jastrow_NN_data.init_from_structure( structure_data=structure_data, - hidden_dim=16, - num_layers=2, - num_rbf=8, + hidden_dim=4, + num_layers=1, + num_rbf=4, cutoff=5.0, key=jax.random.PRNGKey(0), ) diff --git a/tests/test_jqmc_gfmc_bra.py b/tests/test_jqmc_gfmc_bra.py index b6710711..66d97cce 100755 --- a/tests/test_jqmc_gfmc_bra.py +++ b/tests/test_jqmc_gfmc_bra.py @@ -120,7 +120,7 @@ def test_jqmc_gfmc_n(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast jastrow_nn_data = None if with_nn_jastrow: jastrow_nn_data = Jastrow_NN_data.init_from_structure( - structure_data=structure_data, hidden_dim=2, num_layers=1, cutoff=5.0, key=jax.random.PRNGKey(0) + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0, key=jax.random.PRNGKey(0) ) jastrow_data = Jastrow_data( diff --git a/tests/test_jqmc_gfmc_tau.py b/tests/test_jqmc_gfmc_tau.py index 77a7adc1..9cf99358 100755 --- a/tests/test_jqmc_gfmc_tau.py +++ b/tests/test_jqmc_gfmc_tau.py @@ -121,7 +121,7 @@ def test_jqmc_gfmc_t(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast jastrow_nn_data = None if with_nn_jastrow: jastrow_nn_data = Jastrow_NN_data.init_from_structure( - structure_data=structure_data, hidden_dim=2, num_layers=1, cutoff=5.0, key=jax.random.PRNGKey(0) + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0, key=jax.random.PRNGKey(0) ) jastrow_data = Jastrow_data( diff --git a/tests/test_jqmc_mcmc.py b/tests/test_jqmc_mcmc.py index 41af99e9..e4bc6685 100755 --- a/tests/test_jqmc_mcmc.py +++ b/tests/test_jqmc_mcmc.py @@ -127,7 +127,7 @@ def test_jqmc_mcmc(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jastro jastrow_nn_data = None if with_nn_jastrow: jastrow_nn_data = Jastrow_NN_data.init_from_structure( - structure_data=structure_data, hidden_dim=2, num_layers=1, cutoff=5.0 + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0 ) jastrow_data = Jastrow_data( @@ -278,7 +278,9 @@ def test_jqmc_vmc(trexio_file, monkeypatch): ) jastrow_twobody_data = Jastrow_two_body_data.init_jastrow_two_body_data(jastrow_2b_param=0.5, jastrow_2b_type="pade") jastrow_threebody_data = Jastrow_three_body_data.init_jastrow_three_body_data(orb_data=aos_data) - jastrow_nn_data = Jastrow_NN_data.init_from_structure(structure_data=structure_data, hidden_dim=5, num_layers=2, cutoff=5.0) + jastrow_nn_data = Jastrow_NN_data.init_from_structure( + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0 + ) jastrow_data = Jastrow_data( jastrow_one_body_data=jastrow_onebody_data, diff --git a/tests/test_lrdmc_force.py b/tests/test_lrdmc_force.py index ac413cdc..daa4766c 100755 --- a/tests/test_lrdmc_force.py +++ b/tests/test_lrdmc_force.py @@ -152,7 +152,7 @@ def test_lrdmc_force_with_SWCT_n(trexio_file: str, jastrow_parameters: dict, loc jastrow_nn_param = jastrow_parameters.get("jastrow_nn_param", False) if jastrow_nn_param: jastrow_nn_data = Jastrow_NN_data.init_from_structure( - structure_data=structure_data, hidden_dim=5, num_layers=2, cutoff=5.0 + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0 ) else: jastrow_nn_data = None @@ -269,7 +269,7 @@ def test_lrdmc_force_with_SWCT_t(trexio_file: str, jastrow_parameters: dict, loc jastrow_nn_param = jastrow_parameters.get("jastrow_nn_param", False) if jastrow_nn_param: jastrow_nn_data = Jastrow_NN_data.init_from_structure( - structure_data=structure_data, hidden_dim=5, num_layers=2, cutoff=5.0 + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0 ) else: jastrow_nn_data = None diff --git a/tests/test_mcmc_force.py b/tests/test_mcmc_force.py index 23cbd4d9..33f72c20 100755 --- a/tests/test_mcmc_force.py +++ b/tests/test_mcmc_force.py @@ -151,7 +151,7 @@ def test_mcmc_force_with_SWCT(trexio_file: str, jastrow_parameters: dict): jastrow_nn_param = jastrow_parameters.get("jastrow_nn_param", False) if jastrow_nn_param: jastrow_nn_data = Jastrow_NN_data.init_from_structure( - structure_data=structure_data, hidden_dim=5, num_layers=2, cutoff=5.0 + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0 ) else: jastrow_nn_data = None @@ -310,7 +310,7 @@ def test_mcmc_force_open_shell_finite(with_nn: bool): orb_data=aos_data, random_init=True, random_scale=1.0e-3 ) jastrow_nn_data = ( - Jastrow_NN_data.init_from_structure(structure_data=structure_data, hidden_dim=2, num_layers=1, cutoff=5.0) + Jastrow_NN_data.init_from_structure(structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0) if with_nn else None ) diff --git a/tests/test_wave_function.py b/tests/test_wave_function.py index 59caa4e2..3cf469ac 100755 --- a/tests/test_wave_function.py +++ b/tests/test_wave_function.py @@ -432,7 +432,9 @@ def test_nodal_distance_analytic_vs_debug(trexio_file: str): jastrow_threebody_data = Jastrow_three_body_data.init_jastrow_three_body_data( orb_data=aos_data, random_init=True, random_scale=1.0e-3 ) - jastrow_nn_data = Jastrow_NN_data.init_from_structure(structure_data=structure_data, hidden_dim=5, num_layers=2, cutoff=5.0) + jastrow_nn_data = Jastrow_NN_data.init_from_structure( + structure_data=structure_data, hidden_dim=2, num_layers=1, num_rbf=2, cutoff=5.0 + ) jastrow_data = Jastrow_data( jastrow_one_body_data=jastrow_onebody_data, From 0c1ca075fc91f1338ed0c354e08691504c482a4c Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Fri, 29 May 2026 12:04:14 +0900 Subject: [PATCH 92/97] Activate the nightly CI and Codecov. --- .../workflows/jqmc-run-full-pytest-ubuntu.yml | 124 +++++++++--------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml index a02215de..2344e61d 100644 --- a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml +++ b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml @@ -51,86 +51,86 @@ jobs: - name: Test jqmc FP64 (Intra-software comparisons) run: | - pytest -n 4 -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail - # pytest -n 4 -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail + pytest -n 8 -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP32+FP64 (Intra-software comparisons) run: | - pytest -n 4 -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - # pytest -n 4 -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_trexio.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_init_electron_configurations.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_structure.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_AOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_MOs.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_determinant.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_jastrow.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_wave_function.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_ecps.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_swct.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_mcmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_lrdmc_force.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_checkpoint_components.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_checkpoint_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_checkpoint_gfmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_ao_basis_optimization.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed + pytest -n 8 -v tests/test_mixed_precision.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append --precision-mode=mixed - name: Test jqmc FP64 (Inter-software comparisons) run: | - pytest -n 4 -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 4 -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_comparison_with_turborvb_ECP.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_comparison_with_turborvb_AE.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP64 (QMC kernels without MPI, FP64) run: | - # pytest -n 2 -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -n 2 -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 2 -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 2 -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_jqmc_command_lines.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_jqmc_mcmc.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_jqmc_gfmc_tau.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_jqmc_gfmc_bra.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP32+FP64 (QMC kernels without MPI, FP32+FP64) run: | - # pytest -n 2 -v tests/test_jqmc_command_lines.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - # pytest -n 2 -v tests/test_jqmc_mcmc.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -n 2 -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - pytest -n 2 -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_jqmc_command_lines.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_jqmc_mcmc.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + pytest -n 8 -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - name: Test jqmc FP64 (QMC kernels with 2MPIs, FP64) run: | - mpirun -np 2 pytest -v tests/test_jqmc_mcmc.py - # mpirun -np 2 pytest -v tests/test_jqmc_gfmc_tau.py - # mpirun -np 2 pytest -v tests/test_jqmc_gfmc_bra.py + mpirun -np 8 pytest -v tests/test_jqmc_mcmc.py + mpirun -np 8 pytest -v tests/test_jqmc_gfmc_tau.py + mpirun -np 8 pytest -v tests/test_jqmc_gfmc_bra.py - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) run: | - # mpirun -np 2 pytest -v tests/test_jqmc_mcmc.py --precision-mode=mixed - mpirun -np 2 pytest -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed - mpirun -np 2 pytest -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + mpirun -np 8 pytest -v tests/test_jqmc_mcmc.py --precision-mode=mixed + mpirun -np 8 pytest -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + mpirun -np 8 pytest -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - name: Test jqmc-tool (Toolset for jqmc, FP64) run: | - pytest -n 4 -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append - - # - name: Combine Codecov xml files - # run: | - # python -m coverage xml -o coverage.xml - - # - name: Upload coverage reports to Codecov - # if: matrix.python-version == '3.12.3' - # uses: codecov/codecov-action@v5 - # with: - # files: coverage.xml - # token: ${{ secrets.CODECOV_TOKEN }} + pytest -n 8 -v tests/test_jqmc_tool.py --cov=jqmc --cov-branch --no-cov-on-fail --cov-append + + - name: Combine Codecov xml files + run: | + python -m coverage xml -o coverage.xml + + - name: Upload coverage reports to Codecov + if: matrix.python-version == '3.12.3' + uses: codecov/codecov-action@v5 + with: + files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} From 6d144f9ff7e88474da5794d8ab3cb27908965010 Mon Sep 17 00:00:00 2001 From: kousuke-nakano Date: Fri, 29 May 2026 15:14:57 +0900 Subject: [PATCH 93/97] Update doc/UML.pu --- doc/UML.pu | 111 ++++++++++++++++++++++++++++++++-------------- doc/workflows.key | Bin 958685 -> 0 bytes 2 files changed, 78 insertions(+), 33 deletions(-) delete mode 100755 doc/workflows.key diff --git a/doc/UML.pu b/doc/UML.pu index a4bd7733..43093faf 100644 --- a/doc/UML.pu +++ b/doc/UML.pu @@ -1,9 +1,18 @@ @startuml uml ' size -scale 595*842 +scale 1200*1000 ' PlantUML configuration allowmixing +top to bottom direction + +' Compact layout +skinparam nodesep 15 +skinparam ranksep 25 +skinparam padding 4 +skinparam classAttributeIconSize 0 +skinparam defaultFontSize 18 +skinparam classFontSize 15 ' Command + Shift + P to toggle the PlantUML export mode ' inkscape uml.svg -o uml.pdf @@ -22,6 +31,10 @@ class Hamiltonian_data <> { class Structure_data <> { - positions: jax.Array + - pbc_flag: bool + - vec_a: tuple[float] + - vec_b: tuple[float] + - vec_c: tuple[float] - atomic_numbers: tuple[int] - element_symbols: tuple[str] - atomic_labels: tuple[str] @@ -81,9 +94,9 @@ class Geminal_data <> { } class Jastrow_data <> { - - jastrow_one_body_data: Jastrow_one_body_data - - jastrow_two_body_data: Jastrow_two_body_data - - jastrow_three_body_data: Jastrow_three_body_data + - jastrow_one_body_data: Jastrow_one_body_data | None + - jastrow_two_body_data: Jastrow_two_body_data | None + - jastrow_three_body_data: Jastrow_three_body_data | None } class Wavefunction_data <> { @@ -112,6 +125,7 @@ class MCMC { - hamiltonian_data : Hamiltonian_data - mcmc_seed : int - num_walkers : int + - Dt : float + run(num_mcmc_steps: int, max_time: int) : None + run_optimize(...) : None + get_E(...) : tuple @@ -119,70 +133,101 @@ class MCMC { + get_gF(...) : tuple } -class GFMC { +class GFMC_t { + Tau-step LRDMC (GFMC with fixed tau). + -- - hamiltonian_data : Hamiltonian_data - mcmc_seed : int - num_walkers : int - + run(num_gfmc_steps: int, max_time: int) : None + - tau : float + - alat : float + + run(num_mcmc_steps: int, max_time: int) : None + + get_E(...) : tuple + + get_aF(...) : tuple +} + +class GFMC_n { + Node-based LRDMC (GFMC with fixed alat). + -- + - hamiltonian_data : Hamiltonian_data + - mcmc_seed : int + - num_walkers : int + - E_scf : float + - alat : float + + run(num_mcmc_steps: int, max_time: int) : None + get_E(...) : tuple + get_aF(...) : tuple } ' Functions -rectangle "compute_local_energy_jax(\n hamiltonian_data: Hamiltonian_data,\n r_up_carts: jax.Array,\n r_dn_carts: jax.Array\n) -> float" as compute_local_energy_jax -rectangle "jax.grad(compute_local_energy_jax)(\n hamiltonian_data: Hamiltonian_data,\n r_up_carts: jax.Array,\n r_dn_carts: jax.Array\n) -> float" as grad_compute_local_energy_jax -rectangle "compute_discretized_kinetic_energy_jax(\n alat: float,\n wavefunction_data: Wavefunction_data,\n r_up_carts: jax.Array,\n r_dn_carts: jax.Array\n)" as compute_discretized_kinetic_energy_jax -' rectangle "compute_ecp_non_local_parts_jax(\n coulomb_potential_data: Coulomb_potential_data,\n wavefunction_data: Wavefunction_data,\n r_up_carts: jax.Array,\n r_dn_carts: jax.Array\n)" as compute_ecp_non_local_parts_jax +rectangle "compute_local_energy(\n hamiltonian_data: Hamiltonian_data,\n r_up_carts: jax.Array,\n r_dn_carts: jax.Array,\n RT: jax.Array\n) -> float" as compute_local_energy +rectangle "jax.grad(compute_local_energy)(\n hamiltonian_data: Hamiltonian_data,\n r_up_carts: jax.Array,\n r_dn_carts: jax.Array,\n RT: jax.Array\n) -> Hamiltonian_data" as grad_compute_local_energy +rectangle "compute_discretized_kinetic_energy(\n alat: float,\n wavefunction_data: Wavefunction_data,\n r_up_carts: jax.Array,\n r_dn_carts: jax.Array,\n RT: jax.Array\n) -> tuple" as compute_discretized_kinetic_energy -' Dependency relationships -note top of compute_local_energy_jax +' Notes on functions +note top of compute_local_energy This function computes the local energy with a given Hamiltonian_data and electron -positions (r_up_cart and r_dn_carts). +positions (r_up_carts and r_dn_carts). end note -note bottom of grad_compute_local_energy_jax +note bottom of grad_compute_local_energy This function computes **derivatives** of the local energy with a given Hamiltonian_data -and electron positions (r_up_cart and r_dn_carts). +and electron positions (r_up_carts and r_dn_carts). end note ' =============================== -' Class relationships (composition/aggregation) +' Class relationships +' +' *-- : composition (filled diamond) +' child cannot exist independently; lifecycle tied to parent. +' o-- : aggregation (hollow diamond) +' child can exist independently; used for optional (| None) +' fields and union-type alternatives. +' ..> : dependency (dashed arrow) +' one element uses / calls another. ' =============================== MCMC *-- Hamiltonian_data -GFMC *-- Hamiltonian_data -MCMC ..> compute_local_energy_jax: calls -MCMC ..> grad_compute_local_energy_jax: calls -GFMC ..> compute_discretized_kinetic_energy_jax: calls -' GFMC ..> compute_ecp_non_local_parts_jax: calls +GFMC_t *-- Hamiltonian_data +GFMC_n *-- Hamiltonian_data +MCMC ..> compute_local_energy: calls +MCMC ..> grad_compute_local_energy: calls +GFMC_t ..> compute_discretized_kinetic_energy: calls +GFMC_n ..> compute_discretized_kinetic_energy: calls + Jastrow_one_body_data *-- Structure_data -Jastrow_three_body_data *-- AOs_sphe_data -Jastrow_three_body_data *-- AOs_cart_data -Jastrow_three_body_data *-- MOs_data + +' Union-type alternatives: orb_data field holds exactly one of these +Jastrow_three_body_data o-- AOs_sphe_data +Jastrow_three_body_data o-- AOs_cart_data +Jastrow_three_body_data o-- MOs_data Hamiltonian_data *-- Structure_data Hamiltonian_data *-- Coulomb_potential_data Hamiltonian_data *-- Wavefunction_data Coulomb_potential_data *-- Structure_data -Coulomb_potential_data *-- Wavefunction_data AOs_cart_data *-- Structure_data AOs_sphe_data *-- Structure_data -MOs_data *-- AOs_cart_data -MOs_data *-- AOs_sphe_data -Geminal_data *-- AOs_cart_data -Geminal_data *-- AOs_sphe_data -Geminal_data *-- MOs_data +' Union-type alternatives: aos_data holds exactly one of these +MOs_data o-- AOs_cart_data +MOs_data o-- AOs_sphe_data + +' Union-type alternatives: orb_data_up/dn_spin holds exactly one of these +Geminal_data o-- AOs_cart_data +Geminal_data o-- AOs_sphe_data +Geminal_data o-- MOs_data Wavefunction_data *-- Jastrow_data Wavefunction_data *-- Geminal_data -Jastrow_data *-- Jastrow_one_body_data -Jastrow_data *-- Jastrow_two_body_data -Jastrow_data *-- Jastrow_three_body_data +' Optional fields (| None) -> aggregation +Jastrow_data o-- Jastrow_one_body_data +Jastrow_data o-- Jastrow_two_body_data +Jastrow_data o-- Jastrow_three_body_data @enduml diff --git a/doc/workflows.key b/doc/workflows.key deleted file mode 100755 index 671ac59a2098d66e24003f41251fde8f4ae8cd31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 958685 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≫&S)?ze*(%M#?;klT2End44lZd{NS= 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

w7P<_{2Qw2Tb8En~2`Z|ULZMREnDymg`ZX1z=g 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­qYZ$CYuyxaFs?r{&>cj%u_5pFOu-+$r0U8| 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%0DBLMhe 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 n>j#!e#7pd?R^8Qfv7dHkTv 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

14H-Qfmwoh3V`z>jWrRmM_1a6fQL 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

@nL47&JmE)OWlM$D^3Ki1OirBm|NIdZ6%CE-gcO zo+|T?Yte{XlN^3w5k|FfSB4gMzd)zKB%x+vLy9JBSm4gbBg@!cT2_sw;NiTaG^ExO zZ1boxk$SU@G~J+!t>u~V0 zw3h+?%{S?qkyclftxtm=AxbJ`$0J*Yw%ar3=#UYr8T;d(hUI^EoQdRd-B+N3JHS_P z8}u$~ZyWrA&#G;Ep|wo{oTT%U?1lT~QxD+->dd@s0NO zqZ37gO&7o4Ru8C&jMfOGV@)o~y3ffcIf@H!W>U{MBU+rk5n>S68b=rJft|=f`4h!s zQNxWoelD~&$$yVn)#BR9b@lyR}n=I0S)?y4twN(bryWx zl362(b>6NshOeN4Up0)aF#E->l@7#V4r1^(CM6SqLTTsh2EzsPD+6XyW7aYd>VNyH zNr;{cXwX!slU|<~go(vF8X&|(EeG;ZB1F0BnyVH*4)5oO#&!0^RRs6A912i;9x2UE zExtng(HlL=-9RN8$N=`699yu)t~pAZ$nLWqyrkhCo+lKjr&}^tZ!}*&q$g5%d#sa~ zK;x@X>^6T>|L6a!_dT&UhK?8^hiY$&F`Mxuv2v)$>hEn;C6%FkO6u$LmG3_XrmM=z>*d3)vUzxMwy)6C>C}$l zipdd7$Y+J!vAVg5J$z2!d2~L_y!Tq7@X~wx#?t-t1D^FXim+ByU&Xg3IYgZ@VsSTk?fSt@5xS*Y>Zy~?@I zP5iiLSfTGtifYR4Er38ypIS@7JzJ8n?v>)$Z?0?Y?svo3Ff2gk~j zXvZ6pgB4~S-jmTH*GspZ<6q=4&VzPdv=`g-nnVW(Q=**?Jn4=njwod0=+us#=gSjW z?ZKn?ZmHRo`W8bI6}*G^#l_bRk*wirN``pK+XNkE7-67%9p2z$Qp?OOD+={rV= ziE{-rN8mdD7>SBeTBh-IpjJy^_I?dO37z-MeMe<(jfNlKjSO#UzS23Dyc%$S~Yk%)!{qd(-0W=cneV>R3L~(Ll zL2TT!pA3qi@i8+=LVq@Se9f2JHUnoV=(BHYx0B~ogM-vLgZkv@06&;3%YXukifF+T z$T5oVRLp;DXI*f6_uh$%;A{~9+nl{uDw5Vu_b@*VeL5mj8j!Qpl`1I=Mx%?o_OrjO zxby117$ZyiodiuaV#x)#R>MJy;g~5C96-?yhwNv#+tvBrvbmUGvmEp+gt>(k!D%VP z6)YtuOwn(jmvb5)Z4j^GPR=7ZV{X2vmT9??BOybS5dIBXn4x(+i~_ybe*JISc#Cf< zA|hf0@*nfxX|jB2&t1^&c4;uq1=(8211h=d)^|F0%VXUJijuy#uhImELQd zcF|2GRPj}#*?%(Ju!-QkHE%k(atoj9uTf>HIR0gqM{A9a%Xu_}l9T|}OM%9{_bvkf z0Gnt05>3!N8(mP;VH9ELmq!-zp?ThnMk(rZ_14I~dvJG7v7HG8O|tWjgwflk#(9Zv z1>~1}X_~=kbwmD$hlFwX$$ZgkH&N zR2Tmo7SbP-2s6~qa`omOE|l;oq97%9r1maAT3oCR%RP1P<9a?h63!mV&ciOXU-uJx`$0_G-TW_k`#P)kn^Dbv6R(0tfFi+&@8 zXeIQ8#PwYZdN*}IPOyN3mDWxsA2Aw#Jn*d-()bPLiL`Yi<`FE49io zQYEk=VRd*HStiP_0r|7)_`i8?2-}g;1Ah0DfxXG_&rO6VCZ;}ns;R2nQq~UXvbrj* zRF?JVIACrM7inT5VyOppT-}-fy}8E|A#l9E`FNbvsM%_(NL_rWY!g&X_W8c+eAm18x!$w>dRQ0h$$IYlPS&&T z`*;6-_0u5B=?r$W$#kwt#c0VQxV;_UEoT!m$!Lj(yn4!5PhQP$K_C8hJO44Hw+Vc9 z1jD;*aCr_B@g$=s4mZu?*9guwDrMLxdlGh-9WY6h*v$6DEZ;9Jmrrv zs;Mvv?jQSlDH|Ok7WyY;!=}`4D9*+0wby%Y<2`wJ^$eQ_1J2Nz)@8wymM7;;(?PjS_ zlZo{V0Qa-8WV*e@8_=hp`M8^G4L4LG{Ca~77L|rYTjG?g!;#@Kjh7;cqwH5qZdWRX zAE$VRjn_XmhLFNAD5(YP&MtF)!Nly7z#_DGZaAoVwPO_UnC+{JR+VMLhUbk)iWER< z$IiumiDaO^2h_c~VOKI$IQ_o;85L}k|KK5dqP@!_n}dz=Q!DpGdyfP6p_>oH&3#F3 ze2M3J=DHjklJyM8#}!siI)#k8eMu}^&c#$NYFY##M5CN_!%AztvJF?ORj3W7CP&)A$@#meh?%tgn zU?Rt2q)iA_l#(nv;y0tXhe-bDt|H)I(N&Ww0kd!xI;?u~w-EUEVF*~33I-)sS3}h+ zD=VV}&;tW#ohOyMl`E3XQiA%~{2+~JhKJxZTuoDPVWXfh(3?s#^!($x6k!@K-c9zf zhIHOH>9F_Diq5n=J!CSYX2#L_v` zBa_X0eQ6pqn73C_msch_wGqmvxfimjzH2@D28O*@)|)&TK6^3mmNy7*Whp^SJ#WVx z*o$hZQI^m(KKjsG+Q2-Kar4f)yw^(K{<9ta70$$~j-25p`vI>`Vr_T3b1{KD`Id_! zpqYbF={-@6R?}CQmY$apGr7@uNS3*bu_?FeLOK;i+DFeVw9;=jZACI$oG$UskiB+t zVRsJN{-kkSU$o0#PuUt+tl_-n) zF2M2Soa0=3%TAQ*kX0#`b>1v8FJ`BmvNEv>7x<~Y{>{6gjet!lIGE|^;h|X_C&*_l zsJ!JCv`%zE=aBeTH zxT|OyZdq<72V^L@X*F8@S%Ni9dfsGrB-n7?1kSH;%b-I~`Cu8KNuJp+usFP*|D)JN zofs<5P0H43d54tZUa(W+T`c#xnZ&7GhOqz*vYK~V<$98v1A9Bvh)F9UycaA)4jk+& zwD>$5E1TdhY5lndq!`I_wVC7l%ec?hXO2S^I4R%35%gFH)WAC<0bsN?`l@L^L^@)* z6JkZNwiUe4y5d+{0t1cDgBMD#(yh2y51sY!xj_j3{jAGBX(p_gwVGE60$P4OR~?;u z73Nhk2k+0s;2a-ou1c}_rX%+$+$B7CXRJ>=`I8=>5_SPJ)RWi^owWa<6Gzx=GR5ay7!7A)G zj-Xqi`7e4{%3DR|VPI=geDa?(VJb}qlgA?_>~;q{5v+NaF^>h_i9c%d+&8o@*N(Mo z{l45(Gws&<^+{S+O``D>9PHxM<1*bW=OOh;P~mdU?5iYu*uGtY;Lx8mbfh`fj@!QU zJEh{2#$lJ8(-rH?2kf7H$0?kH&?3OQli_q>!!>$E}06X#j~#;y48mi>93o741L z5&m|Mn^InkVw+}Ms4IgC?_m*F8gyZG3LX?xsW4jew4ZwY z51irs2!v9{U3_kY(`f6*^_SI_=qFY7dA;zp{1lU_YVL({eft6D(Cibc>{8|NXpM@f zc$IJT{&0uEO#HsXg||~SWlggE;;AQLa)Spl`x0_Z(CI+Z05zPLx>{)?xL9iCJ_$3R zSNuL3zyEI?DAZ5ohK)9!EG-59n?O*ec}eCRLSltPCNP_(WqTvFCf<+rrP=LBwi-}faKmdX-<5J z;t^AkNvJza=%D{VH25@{%8;L78-=q$=UQ%9b|FH=T_+lqUv%{kj6ur|!PjMby+7fo z*!Lpq1FO-kDyoQyl*!(e;vxN}<=%cm0Xd^Y`ijX*jsScW)rxvDnx;P*Ypm)Ix}aZh zFI9cXtfy92qvrTKfd&+!z0=0NQ@AzRyX28&09W zVXxU%pp=M@-|uV}hPOnWW>Gd(d@gM(#{pRsYZrwoSwIr_U|GlO{ec|oDY{OZy7)j; z?{m*HGL~YmFxSB_j8h8!ZQkkt`^U!8fjfqh4S5XOpImN8Xc=wcduttq)A?4woHau9 zVpjM$57@$X74pL9Sgp!;5)E>G*~>31_X73YPr#fyccyKHI-H%!0!`ymRzN z9gwjapXO1F<5#B!GN^n8rQMC)4Tq`E?Mle@K&nPf&PDA$k#fTeb#x$yF62FVV(W@l&5IJ;E+^}d_ED+LfAyZWm&=06?mN*B5pS4Re`!>sIplX~qv zif;EuM%~-&7amRT!2|2_YVi)nf+c(PjHjr<3TMpl8>29iI97%JbnQh=+I(G-NZ^Iy z#Q|}1{kmUi!}u9&C$SgmEII(=RJb=NEXur9X4x@r^CLza`g#W#kK8w!cH5~;=2Zng zyq`VVNdM@!OUc1{s)8cB_t~;HUH#xAeh8s>ZK8%yvx_LRA=8C zmRl1{3%KUqM;^ynX^5`}p=XzRR&|$Rh|*UNTvV5&TmzfkSr#bzuNvJh^SS5p<|a^{ zL#{qdf&BDxr>|B;cpv=>)Z_k>6R0Py>8gtH+~^PtX?9a2wu>JshzCqqNAc(o1l5bn zvs*M~yj76|=%d6DIt&1ZMHK@3BQPh?eAH)9vny8uk1A)a`w z25gg@n?1P-wfP#6juKEGRbCqm{hcySt^l2m*Uk3@f#NQ1SrOLTkgO2QJ-7G0pujZ;mzP+li z=BE9|TDn=VBJMFE{6*CH=}d<>d21_-&n^FV4Y#ssnJXoa_y+Uc$2-VIy#M^CJ-A|N z?~nZOHnz-kX{iVD$Fn;L-Q9gU<%v9t$0_V>G(Y{`aTA~$W-PBt<;m<$q(ed#!O;1j z7r?Sa4#gc1l3TjlO3pSD0XllnpeWWe#J7{PE9Lcg&+j71I74LWc*ePL#@x_q;U5e8 zpGOz#-LZhjSyDw*JfesHSKTPljJ2Lj79BU|=dKb<+H+w_d$32Atbd7xdgr;YUAx|b z>DlbP;b%Rk;{M%$329~ZQ&V;@SK{@J@zax)zMZ%#WHWHL7F@P-soh&@l@(W_sYXqF z|1zL%nXig>KG)xtPa}&n*EB~|!GrS=8(gi8#_MgQS)c@~9cl?2D#`vX(j@U^<=wcI zX%|4nsSqzrEb-ik9I$ldauJmhW<%^Z2rEyl!xHqPv^eB1Z=xS`xo2(rtLka05vo+) z5b9U)c|t-44ZOc`-8k}@FS@eetXwm*L!OZcJqFM4h+Z2~xYs;?qf)JVe&$=!Xyrj@ zAW-D~W!TL4zFo7Y8?}TnU_401!rO98N4J z1+})U==^%jC|MNd3{EP2pIMT`Dz#6ccRkihU8nARiRk>2I}m+0<~I8!dZtaZEvQ@{ zS-EpY>y(EhV^}{S+9=V$r+6z=;;4#D@tnBBQQ}~G=)DoZaLWPNV-K!X0TyX~qXX22 zl+=9|(+E{ikM_r=iF0Qk_p!R2FTZfAJU{&ImzxnbzihKgM#Rjh*7zJhu8NyxK@MSR_Qk|H*Obe zv%zh71D%3D2NvPO`(pFV*Kfjx&F*aj5P74Ac8>Jynq=o>NH z_dHm15q>nupoQ6)Xt%gny4>AL7rgsY=U5B&hh9FRC)K9-&d5K~N_I7(`%?dtj(D<~> z(CZbm_`pF&pUM@exqAw#xj(6uMSPjFc94JGWMIn?{3S4%0TBB6a=OKzImlCTH8~Ep zy}0O`#iq?+S0k}&mj~m49fkAm?O(AX6Lyn6UkXvt|9Q!j9+&3B5o3@=UTEE{f;js~ zww!gNacR3uSFC+l1Gg$_TLplL6_tjd-8(OxBq@iOkk5<7GI>tkuNSuz)1~u*nh$jF z4qM=}`@P6V$CqIGO*Vb1=;$pp)4_f_*Zy9pzrtCd;3AWDFCkDM9Bsj{LSE{4Er<88 zY5jf*EJrp6b%9FSGV=VF2izvZqW^Jt=bTaI4{&D zbII~~qEHo>QIY${%H|z}H^lneS&);~$*G9YC<=azX#g*V-^7a_cIakMyrj*@rjZAB zL&2NZnjg&KS{xGRu_v;@T#;Ogy1s)-?gxCu=OG}O&-u#ZjKc-ND_)i#>WLk_JM; z*zTxtCOn-x7`j1=(LW`cwJP;CBhN)^ng|4|R{IGDty;~e0%3SR}?-iKcHqt zpLD+4i`QRz<2Ps_VwKo@-B15XTJMLQ3Feg#*Cv}_ub10GcFCv~!eF}lqF|!eJVblE zrgp+n&aQDg^Brv4F7<9;m((8{zCQ^(cTurS>CBkS8epk%6dOL@Bu3?h#GZTiD8o0J zojC!iRfzm9Q=N-2Gu7=hona02u}z_&2Wr#R`*$7Xn9uGkxnL@ky9Kv3y%gvc{}+q; zmpXl~GGxi0g&cF?MkGSf8S4@6$*~I;`=TEcosLH_0X1)^uqJr%oMZx`@=-dAxXN@LpB8H1R1lLqc&l(FqF zPghkC^O3?2!D~{00TB~|RYs?I&Ti$P>&-6|>cf;3GR6&RCg-12+=e3j%KSMS(B|M* zq1>e#GKjg@i69-if%tSOsi3)vy9QRgC9=DI>8b6KYrzlKZjMkpmh%h>xR_R^54rrx z+6H(6c0ywvbHnR>47S~lw}TFE6}|z1MmDB#rW`-M-ecwP8MmL15@1SpE@jSaemmsh z!kdBOL`hP94*9Y>zKHLtlDbwVWm(CU**lW9AgYRR+pkR*cBq`5wg;d<<&pF6kRI@D z#7R|2H$P_HU)Yrwe&@fyr8iPeDMYlg+s4_|N0jfCV3H4)O`ei}%OU1SW?sJ1lG7wW zPpg2a4FRc&I^DI`(5YW<-NK4CwXG=7E1g$f2#~b`y)dT?LZufs_;U6Q5p?6OvhABT zJ}KMzfq;14T!24fSoX14?Q$PcCb$oXl;lV6 ziSpj$QZ<-u6x}t2t6}VF-EwVADs<+v?X*B`SRZ%iro>=3_3BM|qkB|LP_9NMI3puBJ{&LwUG-AlY!S zUa#DmsrHZ(eP-v?5BY8J2F_(zY;@K0>TKrjvHQA(PdwTYh?5&{7U+gPZeBX$xJziV zvs{hVa{Hdaf_n$T4MnAgY7U~W{rDmC_No;j1H!1>lD(p8HrvtrdtiE?#WjPl0S6D0 ztpXi5S5CfIk3!N8dpu9I*^`+%@$C}*{>B%AHHMQ6j=#2*81AP10q^I}WS-YF8HqczZwxr0kyJ@)CBR6=@|y z-mDf!h`4iLTNSuRo9d9rtdoeeO-hd8@=T_r+JYHezPH)p82#kNrk;UI?SiSHQCARp zgD^m^q8AckZzMQ3H{f>f7Y}<2?f1v!28B1#K5@b6AH>1sge08V$V|W) z(ZRq*Q$h#Yr?m0`XP(x$4oOpV% z>dIYs?Kb?%%ewNT#4C-Dxu~WJ}9CrcXDpi zuV2yDpQt1-6b?5#=hk^)M@Ah>1h}}XE2g!ZTWb9tX)lH9KKtBOSULf<^_h>HC@B}} zGYTcRZh|ie8XHIt!e~JE)?NSMXg@OURSr??Pa3m@RO>TSpLpK37o)SQrY~CtJoRk`kRB*8tMW22{f%OhiBfjZvKQyM%flW?QW@l z*8+T1e%oXxKSue{p2KwiU}ZYCU+ zE6rNt8q|bH-CmCg`>k+6=GwdQ{Q{DUj(-<>gRU z9<4ipR6+rP18daY{6GWCV>!rI(6m#pJC7mD-4GYb%W!U!KUCMV6nC*daQ`iJS6?chl8SfsN%oridlUs_SH$AMS|{oUIO$oCDmJ zGp(W4)YgXw$`a7#*}d5shedZ(z!o9hb^N3aF~}B#?kZ4TJ}!WZDa1+K%>`Sit?8@1 zXMXd;$+LXfdJiSxxs=L1_jQf@A4Av}x~VVx8m}Ha?t5QUjl^lkkzB1jd9|o`H5WGY zyPtb`ey|ff3NviGK7M(9(u@928b0~WSpv_)outMG9{Ect@Tv*l#Kuc+LQY3RjqEgn zWm4<&bd86-RC;L4)iv<9f70#$A6z~jzLven{JrCT$T%WORN~-Lzo_!41^B+LNNRHS zEZ;{9`+%1!LzvE2FlUY0zFaYPksnc!N)p0cODSzeVn90x`mmXS7)s0l{V@Nc^tNfn z@ytm|POg=^sng9F0gQ#2p8f47BoXzhmQU5UI_u(olUj})9b!ctk#`qz*SvuB1glI9 zUCX|9tgdqntWOlk@z%;Uy>Utt412XFVX4iomVs-`lsA}8A`h~*SCt7~tn%amgaUCx zUr$DvNY1&lzGqBqvgL(Ydo~cGe4(nAMF6n`+hB*-HnZ*Cl#h2|5UJtgK0QJZ($`eZ zzW6LMRc>3U%5GnJ!p1HRb`7NLu*dN;F%8bsvOx;_S3+>PM%KHfKufcJjV(lh>)X*% zsqNjkAIvq@=J28TK>Tj|PJ^=HZmU4l(xu7hHZvx|`#z=iMASg1HppliRo6Ug(>vSD z#X-n1mYG6Ww7;Ko1HNA6l&^8(W&DH2#SUG}BLP;GZ3`b$zP)ADD@iab_DZLM*Wp`d zVCkCa?ySRQ4{)p9Y$(v9k>1A!mhKwNZpBzAR>O1=m}!^?<@L8MGz-+AY6|Acj*Ixu z37yPqw}14_H#63d^J;@t$1`HzX6rm-XJ%Heyx40N?W@gpd+67iF{fswZTw;H=|3A( zW20-&@V?9as1H~@{n+P=iK({AVfa~P|1f1IRZzGS1DtK?f>XC~eob-NY-Tr^UF7Bf zhwb-UBHSO>aTQCk zFz1?0)n*ygOJ5HSou$1&k?jcXPzLB08SW~h%B2Q_ukwK2a%jrXF}-?`l56CX=45Gw zngJiPojOCDmndwuRI9f8C8e#&@ap}%x(|V`H=FA`W!BlmLbsJ$*Oi|4oWR!+Ax*qp zzhi^S1n2Bt#3u41i#L+7@>P6dIzO(KrL2y|viq`0 zY<$n<%6_-;Jg`~9sV8OLHc>!2(gb0bKoaR3YjhE!sx9FZ&`&S_*+}^BzdnL>V|-Ed zD~0ns3RmT-s%H0waNB8{Xa!n!uP+M#M_w@SG500gPFY8`h0wHJo*LL zcjIA?Om{^qN8dEiNJy9;_#M&oh7MY>E3hCCw8J|)D?9O3E!{1q*&+@btG(Rq@;**v z8#X3CrqbIRE}%rEISmD*HUwZW{xzbLW%u%)VWpMkOo$KFNS_JCMm9YlovJx_F6$d) zbUIL1r#f#wH08b@cX^-AASBE+%y>E=x8-dziP_jNSA&FP0!kbMLF*9! z#yYY_$FFC0TtY!eh^-+}G#3I6tpWCT1tk17E|aU`@NP_cF=*TL%IZCG2BA2cG3Ad9 zo7T@U7q;Zj%Fok~?-24K4<||4!>z|n>zeeQ$4xrwaNp&rjd7VH$|DgLXu8HvPnc!#v0=_0aFyw@eSZH>nkDv^V;#xvAarUWkkQZ}n0hQH-CS zbZ0%HDF2kJJh~sw^(PHA(c5dgK1c>``^ngX-Ul=U3*K0@vALJS6HvAlG%YeA?klkE zG&>lJ+-~Qwu!0+H?#=*B>KR7D2TAP zvA?;F&c5!e@W}1CR`NsX(nZ(q5>T~*HnlfT{u=Jb2}t+yx)i`ZAegVq6ILf2o)pG5 zF*WHv@p!!iYjcL)wDGKb+TyhwnV%|>sWlkXMyMZPZ0$JkgAIgf9;eE8?x{@**p z59XO0(F=FmLgMkpJT_oC6%){ehI;%NqvVs;3 zn8Vj2yk!h(-0!-se2sc}~TS3@+(%9!flT2%YtV8mGa24fsMM_}lqO zm;~%lbquBe&~9YAb6ngMOA%TX!>b*;=9b<)UVv`p&t!~lYkq{T3_0x#19d|bytfu| zc|hUCN;I_t$^l2XIz~Rao>=CLB6K`gu>`j=_)bQ)eWazfl~ab5K4Tj6K9$1L;r{Wp z)Bgf*%3KkC`$P64!D26@#?8{8Q@HYK%NQ{m7Npap_MU zTxK}FFDg^;ti54)I?m3%rKW28-NX_rpUlE&acI76d9&cOZQqfXx1La`6Rs#*hy@>#@h-X1+z($3irMw=mX_6=+fZqOUux< z6;Qe6h*T2soq}+1ZYX#jL9&C_e{C<#XBjEjDe$eJrf(k^Ja{g03wvL`PLggbg+nJ^ zabg1Ju20>8$)lgHA?dr^wyc0%?Os;&=*1QghUruk<~|GUL(GCfk@M=QqKAk38}fU* zv4hbgHP8D~Sk|JAIIU``Jw>(q{A#73IG2%vXnfa(7YkuD|7z;Q=PH`|#{Vqa^LhNZ zf(=E&Z0uNJDdB-+x<5>L=S$0_jT?P~gus=InBWwItMkPr08MAojlE3eU z=nQQ2nD(s#$%5&DkfaELVOT&qtq)cfP8)5sWR>{_n`L~bIO27#{$zs}hwS&}gW#WM z=8*fDz+mq2Mh~!$7=p@A*phV;-?P5n6FVU|Z)Itagd;oztS$ylmumw}^VS$-TUt7o zI_dRv*)lRRQu)k)Gnkh_posWSm}GX&`%iGZbBr50Njp}qr4uDhaC;%NPu^=-86mJ}i!P&TlRbTsDb z!uoWfv6HRp=D>&l5>LX+h-~K#J=c56LrU=beviuLmoUv58FzM8MQ%nk)^|xSdR67%D2lce04C3s2OWUe`qI*geG+=Lw+~F2D8_`*8qqj;LTi#~Uj>1e?&A^UdeyOFN97qOdmq%G9XaW2ERDVv78Im^)7P==R@txIx}`HVc?b26CQXnRp~x!Tj*Z~r9`@ZUDcwZap? zN3H(A#Fsm*!tE`al4GZok72N~jL^m$XOw;2eC4jFu)1xMyEWx8PTqj9Nn?m@_cUwg z@LvdGndFs{1s%H0`isU`GB$oSJs9_rJXlgQ!X-4Nq3jiU9Bx@ssjW^%d@etX`(l^k zTela^fC<^TvS)DjdC%6nZkDfv&)y=Vv|e#(gOJwRcC~r)K52y(XsJsFg)5r6)J@gR zL!I+Ka5;x)vtDkTvCo>I@}?q+ds&I=TCHe#h}(4I@cZjU97Y&UHR#Offj)4$1Ui}5 zGE)JuE03@Uenlq|(bm>7S5tkem$EeZdKN*eLpw+!4c-J}%>029ix5{YFISa0rmE`4 zP)+Y`Ms9DrSOPP5(&|6NF!hU}=B$e=`6?-(+#vj9KO zmgG{ui|e(&<+r&UIL(i+aB5uFHfo?|*hhRIPoEIM7p&*!8yOik{vFIefYFG+mNZYugD!rmRv1xXPBh$5Kou;}o@#AXtO4Gh=TD1r zQrqOKHz+UgSF1s5a;Kb4nm?CTiIYetnSRJI|HtZnz=f&5vvpbLVc6+VI@M-5wY8s<4!*ka zz8rX8H{E3U`+ETo;o`nTA?DVfAE(R7BHmaT*ShpNtA{1X=KUcgN&J(h^B@zCU*Gh= zi&V(s&K95^$=6dIyES;fElcaX#5Ro~FC(X*hSZL9(_9u?OPvJghY_vJJ)zE=+Ug86 z*|%4DsFgAfa(2u4#cnX`93CYq&IVs&tlE&BcFtaFb2~q&t*|6TSvX6FVQVmTuP&W1 zwjI^bguwt(X6Aw60LeK`w08Rt;NP~ zD%-ZxlPL{{a`o?DZWtstYeewO#_3Hj*C+D{d2d#JX-s<1BlfV2vG{vF`*WeOViiV5 z1&?NS6RlTy#*%zm2?RqT8NOSet?xN0__zvO*^B_c1A)HMt{goT&(V{q#$Du@AHPiv45~uK?_I3{sz4Gg$ z;LJ{3hcG@~JpOHR*y3C*Jcvxe*UxIWJbrNKJMY1n+i=kOV-C1j~K6eO9C{vxSjzmXRu{ zu8u3x5V_VL3VIR64CM($&w+XwS)gRPFcs_8RibIxq0N#ka;oiMPH3aS3Gv)$zj294 zCyO+KSvrDHR?Mu!fi}B`m72m8B5`80c$&kRLUCtErD&H6|>sgc;emwehUc+D?bq?wff@y?QZU?!gm{^E`ChjrM#9 zidalQJB{8$%^Cfs7<67z6IWkC^YX(DK1wS zx%XEHjHv4VaeM0@w*B9XWTFW$0tDT`FVE1rOmdOm&F7BwS-!u)-tHn&K0|HnJH)Hn zbj|Nb70rzFR(+ZuadFT5(fdmv$gXmT4JdxO4bE7xJ`AZTs zI|AXEXmHT}QVG<)%^1R3wYsA`QN8`xVS1|`j{Srv2{ycJ?>PD7LN2-BA+`<&raTPP zT6cN7$4uZHY?^{0Z^9RL_?nZ?EWofVcSt?4!e5fY1o72I_-tVVC@;r9Mc(^?q>=o9 zGm^hMbgTYr+tlX?hZe*`jikBw-gXG{otCwhif;TArLOy8uN`9 z4Dt>MC^hUfWd2O0xLjQLi{g^I)zx03*8Md7%-76Mav27PHdSOyT@GsZz=ip9eH^tc~B z0xPx2OG+76Z(A=H_NFuLs6>21@M;V-n~pOBoO~mnF8FuL4VXQ-aKkGKCg;3tT9#iB zlB17$wyW&7pRQyBk(sY3zS@^!u-(YCwAlxYLq~pdVF>zF!`>})tLY?+Gwz{m1`WML zNLmPeND!IYi8B|pENLctX!N=|W|>cZ$=*$c?;t5+gB>Z4auqb)uC)rD&14F^ZnE}L z-y&L)r*-+mu~+KVC=cx`)-6cA+g zLv1O#Y`H&O%c*UgnHxCdG<|*g+u!{0FFCaLhlKt7PCP}G0};*|k{FFt_~IdJnoB35 z!r=y?cCL?L)v^OHr$ObqZ%kG4D*OfI8`Y2zE+)I#+66JIxggD3vtfoP=4C}?u94o! zHoqwQjqn7SW$)MPD5b*Wgyo{C!cnkK+xtf%we{^$Kcru_x>nZ==TRFoO3*b(z zi-^}26;^e!$1{isUC!h(^nm#FFzz`usGGv6_70-xuiXi8)o#7paS`>9o2oxAh_9+= z3fkvXbU-%*)n-<{@7Xl>HNU5q_6JT~gp4Lh$%-(+5B#tDfUbt37#9<|1f<@V+Lw+q z{B&pe?_jVWuc0zs2E&t-aj#?X`90e$a(tVl$kc02hblofTr;&8udj0tG*UUz35z+; zHOCJwsvQ*`d4cjRco)pgn_4vLCk^+^bC{1m{1pp=yyxANrzAlsY%>ZC%_aqBD#yBJ z+ZCU~XTcUb??==3h!AInh8_e~D|VoisPemUc@#|BFVNpM2l0ASxH1qj|2z8MX8LQk z|LrlUx<>A!5|#u#H3~joie!tr!%2ZMOq6+p~shmc0Jnds`>d3uOFvET<{Rn#>qhL z^alr_qFm@j_(4OeYlG!L$LwBy6ZSyNDQXZS* zmLF4qvFoV*=<_1jN-<}rU_?Z!66EHw+IkC(=)Ctyf!mR7Y?(?Ow`vl_VN0cgW*ea? zg>Gu##IUQoqn=THuDhEKAAMhF?oecx`mH9C1+zSWl66;i(_iCUyJHXB!Pw*a+7hSD ztVH%VM`=S|Q_@Q`H)+BY?1XgA$B0-PuR@PoRruh3Kcn|z22t@KY1Oo9wZ)>h(D%~$wMMAL9<~#- z_oWL>_|w0w@?Um%J%U1&ndE}e0CcT7mPBP_0uI;Uv67JZH9^jbn&0gGJ$H338G||^ z(x1&Jrdju$3acMF|4gbzM&`g160IFo4p-Gzs1&xJDFPeu1LnZ9IXQFqgpya16_@p+ z+(dOm%miypOxE7UOWy-`1359_islVb4A_5f=!uthjnzU!i~49-x`{k z5isW&7d)%I$GM1rA1UMOUe}_}z^5Vk?{I?zo!l9|rPZ8|dat;pJr@m_<}ADW-d zUPkM?6@Z|njuNpef_TJ%ouESEVlcd;2M3Fi;kY;Nw5L_3V`Eisu{+~lGY6*_8wcO@ zn}W+Qt!nu7wL2g0 z;hab2QtiAa4 zd~3qK-UHa@2_67jg5wPXbCbF!v@a`o!9?x${O1x}eWk=#>1 zbo}vmhySm}VUqs`@QS<2f1sl5sO)XBi(>xVz2OVWF;OCp$cCa#xm>UUO18K#-CY>O zPxqO8!Mw<@@j@lIs^`6Nt_Vo?Qo*uF?r`AAo%iGqAuQ7rK$u}Gl4T>OEmSpT|H%n= zPxo8iik^3F7k|<`Ed{+V|3hs2;yuQ$>TudD{jrER>cII&A1VQc(H(Z1AD#ZBiC9kG z+6ePL^J4N6WoC~ZU{IXFly~~=2{ngmd>n%Kv(U+pgnw62%H9ut3|(-SH<~n=0H&`z zGJfX#_xA8>>}|aoDZ6-=w#u$x(U;JHM`VmcZnxv1G-%Iqk~H;8cCMAFSiN4nQ~0v2 z99N&AqpnpguR+pKw)gr=*V)mZ`6z~gH`idPT$!NlFbm(B3gCEiE^90!z#7dmRJV6r&w%%_4+9R=6%PUk@cfY}x2 z9*=;MO8C3w>`Rt+WYCz0hdsU>tPl9~dGDCMClBb6FV@MIbsTi>F>qp~yBx{v+}Zw~ z2~=KQRaj9~R1AwU*3+@ORr0eXs1 zg-w&{v7>_?1LHNpJJ5KDdC+MgYP9KNt!wSkC;O)KOFK=B=k9KilTKiT^Oy1-C;i|w zc9|b-QIVOcGuFtp@S*^$7+Ot+N9f#!+CD!?MZd)hmU_!&cI?d5XaL_YM2TFG>1WS8X06d ze9%)hW8Kv@9yrqjtzg z_BUEdU7=DfB_Puk+Pti;XKXyEUwt=G+gP&KROKXE&RzNK&FS1lEgAeBG`gi42XNU- zELFPj;!t6|Gg+i0achJR9<{3k*t_IK#RZk`HJorzfmy#B-XBI#yz?d)m(bDp@pg~+ zlCAB8YGl79y^v?GNro+u$j?Pre@f@}6(R65c*Vo}nlG}^qjk;-mm zfypH&AD{y^eK0_+&#g4nt8Os@y5muN4N{2P|BnP>Tm=F1uG=lVcLGp5){(5Yj^> z7NT;tWgaRqf!az`UTL{*{CThwJ19JM0Y+x+0KLf@R;TNGVX*^skf;2a=N|(}>#%GN zvp!M5bc))^$d!@>43Bad%imTqX-`1u>a{2ON4NB1z7G1+jmy<3=j4@tj+f(^vyHQV z)Ok1V6ZYvpqT&~B3aH?bGbaB$aATU;GF?!A4guj%REec)Bh?rk+$lb`HD80v?JTg* z>%@DxxQoH=>!f&Ecr8Xa-83{yx$a8#3Di;QOjIPB5;7h}(>=rG7wOM}yI&081;Q0` zRoST|xFs&4f6_2ocWlbBB-KG)IPiGyrd3j5ShjN4eMRIA_bQ!V)YjC-a{Jyqa+DFH zB6#MXe{SF%NI>3CWzk{ar!sf^MNFCJBHzbj_>ViM`LX_R0K3ZMjw^89Ua~!(96CgR zu8rt5`*Mh-j>4PI5b3t&N^HBdikI==gon98gw}i$jl(w$T;+@p7rUA*+q0~FA1P(_ zKv%97gN@)ztpxQt%Ye-xTauD_2(3Z7l~NnXTM{S9#Pq(|=;3ihnQrIS=S4v63E zy@~p~k;Y6hg9gBm_%CRt5B=Z%wwnK+4S%8ummdi=k0AT{nQu8&W=^ib07!qK5l8*1 z=mPP&-Ot)}iEb7Z>9Ugw)B^9ivBdWFHpKA5BaSq3Frqxif9R}ty^XEvTacYwu&LEy zTj`xOmED)vG7%4c$$jP^d{z$g^iK3Iv#|Wz zjktZ>Wa_!S55Ze)7_k0kMJiq4NuBotcyX2eN#p(I>^bhX#*SjUsm52VF$;%vwW$de z8v&L$_Q=z2H!N}B)p?>g7}{_%Ui>-wE@ zCjYFpv-e*6dG58>-g~X}tovrUzemX{IPGm2m`!dp%+%{$#HLGiSxnq$@D00W$V=g+ zf}sNaQA??elM=}qZG()GnOF(gKNv%CrXS4v2oRrSS8E4cQbW3jEL{WfsCYp1OCFgf zY(Ow0QmEr6XYc-a%D+y2eIMJJ0;+bAHqf3cnQ}Ev@Eid8C3&7|&ZL_-&9c+e32cfA z-)6h@4dQiY&asiz>7w z+;!zR22rzi5>&n)^{j#Xaxj(H7jiu40R$D_;+WGUnx86Ces+!Aof&n3gVL-hxQo+( zYcCp?#;4brwe>R$SMQDZ%?Vf_Zznn25Yj1Yt#KF`q0|=1z}?;dYF_$JAchqZ;zpCv zox2S!HNEHuJt;kb>K>~fpzw%BwdqR>`Kk13`$)?QuSiCF5Acy1CYdyb>DU-!d}ia}w#D*#bOq)O%fu67-&D&; zw@!|Z#<}k{R4d1Qm$aO~VOAXijo8=qaZ8pbd8#%^GId_Xk6dNRLCWUI%c8Ds1!Cd1 zDL?4Q5Q8V7!FPKkpq8mgY(h3aP(xXavN?vv){V9uJU+ODHAb^(Y- zM0VdU_i7k7tk;jU0FJRiOX~*b;M{wM4tJ@j-wPtz9n^3$s$?D-j)or;Cd;y@!azYf z=&E0heB{8Yg!gL~PnM?s{cdmR&FF|?G&3y>zIxSeNo;RXKIK;M$|K-cKGbMKq2%BT ztSRUFyfv*_x(R-XBBvVD1;*gLU=2T}*M;w?9z|>x46mo7a%dOcl;SXVyO<~bP-|y~ z!IB~33xFDa9>u|$`RAr|WReN82H!k+`fM%3VGm7_r?dp>6!THT3z5goqxy`3#$X=h zLseaa_vZ^zHOyf?#GsHKxi1j%I!+y!D-LanHD=xP6H*s_xXjwerkQ%_--d7#y zA#)TAEbt<0Obr|Z_1y7rFPPp?W%{X|D59k4(kEvETXhGdhO z&PO8l6hC;$-}P0?^Q~SeyhwhfcEp(!9a@5(wshpA^M@1m{kb=J5 zxGjk~!&!`fNlOxh?3YlRmhuAe^Xj=we6JdT}Aab5IMl$6NuT*{bx%Sicn^E`qg0rS8{Kk33V>>X*ZAW$I;#?%k=38aJ^RHY}J$%F6*} z$`WCkB^V4_*=J7PBO9M~o8h1*O%e$S*7YqqNJ|Fc7`m`wZ8L}bLteg(#=WAi<;mGq$@6-#wZ(#3X(U)8cRP?RX&N4%jGv zy7ik07u>BVo01M1K^Isy2Ct#*f1vM;lP-*(4z;l9{B@2lID3)1wkX_<`K7*5MG}}Yn*Roy zWdxaG-uAxk7G8t8>r}O0hh1_fkvRe8%@W<-bVqq##1rNqpu(eM5U-M?P`QlY$iKK=mqR1ay$WyQc$%9 zaFa@1S#P(#96cB(lQdcKwvgGyjmXrFrm4{?6_b110BkC?_4tU`_&TBGwfI&}8*E#6 zibLY?1ue;NMTYG&9Md_+C+kB{irvT@L28#3?YBBtq3m2NoW=POJXycEO(w9LW4W16 zyrn}Yjl)L5J-swlG8s&e_dsOzY$dj@8!}mIg@jB!n>p!AW96$SD*1ZCL`UoaHf_t> z1jEgnsvmxEn36pFz|g?3-G;#oCcn<4=(dsZ?b@UQJEUu&r0=9;=OXD&YZVA7z8Q~k z*^1^26x}|SPV>u%EO3l7*LSRx>{F{uPvX=pz$R371M~6=oF~yPKQmB(pDRwAngMxC z$MdqUD7ybDw{@HRBuBdE^OA*DgicoOn+qMSk=#v4ZX_U~OegzAgVG$a^Fs@v(Wp=9 zOEm^`FtvKi^9EQBz8m(rZ?J~(WHn*IcC1eDj!mMSvB&i1**`VSzu#XM44?uCI&cvn>dFPS}>SI%_VV+<&IgIQpy*h zg`iJ{E%T2INp|{W4NRZ_f`hAXk#@t!ICv{}nO;}umsQ}G5q6bt2Y0Ud7BQGX4N^C4 ztiD1Watx9}eU9Kq?EwZp2@}U4{}ofv>(+HykPGIpjK+FahJP!Pj_6fs-&rd+-D45N zQmOF@ciXn`TFjoOk{e6Cp47nlC7b#4d+mf`OMQcCBThUcZFszG>wy*01lWh~iks34ECvb(`E!X_ zkxV7O6q@4D2Fh@V@$Mvge^9hwcs~oQBD@egMV|k?UQqFqB=`X$WsV`p;sg;*+=(X5 z!C&k8j7PStVP&%>4f1p+=h$C0VPKDTb@g)JvV>VNrxo3Dxs9Jr3}bHZUm1}^<(!wI zc+S2H!s-^2=8s%Q!yZ&qIz`UybA!~Jlp+;3n_V~D*UiEXHc%r;BG%<$tkX4R&Eo5) zZ=gf3DgQvkM;`d=S9Qe9d6GHpYnurJ{QNb&T7|rB7;{U&ublR1{nC}YNTH2bF^(lL z8r8`f8c|ca(fSIHjYbyYRW+f;#2Zb0zCrIA z8JR_@TE1YUng%U*PH(8Bmp7=-j;7vq0$wnhjhhBZ*$CK-n_jt2Dm}(3;dPgPP#6;E z!y;6_eOkY{H&N#&!<4eOHc7Krrk<+?snTgtsr>S|M59qrT6@X3wAcqZdi8ji)w4o3M}%wO;|xFB`$oNZ<=w-{GhtHYDhtm+o`juk zSz!J)q!okdhGC?k63r*QV~3}XsRG@4FE$gHSBG(d4r3OMxAn#9;p{~w*Kc09AK;E} z>bANGkJ^LW^@nQD`{$Nc(+3WSW6Nq`bN3&wL!C@$-WYLJr^KBvg)DrHTF)|{#+fD>)wUJ6I`YHvxL zB_1E;pODy;(GWXklmh0=3h-UTgKG;DPt7VuYNhDoYD##08s~)Hzn*KNnjCk}d~l<> z-)S>ly(+N_qCe8D^L^eD`uQ#&#nr4c{<(|ESsHJnRTn`$(MoGam|pSg*dt=D6$+?% zXjxKbBHaf>A{(ZcFvsA>Eg$v)DL0b^c8q~gqwJ2X_IbNMb}|1@m-l6kyVjrkTO3W# zPT3`6%siLm^QHElqNdfAr&%tz0s0~zo;X~`tXQQd8(TFo=%*M$K)G4pV0q-&wE6Es zwM_-2PLi)#RfAT!L)=tBQIgUL*oYtG=CbEKh$zl0+%k7~39#Bb6UtYN~K-aUYJ!l z!eC@!#dB{@OR`+(n>KsSZuJV*w)7;on>7#}T&!JZSbCUh4Q~WKui9S^X-x{0mBus) z+~<8uc8)yM?Bn2sr*wg2zll*Ek&)jLTw99PNPdkKKdt1y0b9%899bVrPDqDrpBd3c ziD%f7HO``5U6mvOgM@xi2t&Ne85Z+SmC}zw%QZ%&4$a4aGDv)wIHvHmznJ})DNc6f zlSIST%~w2aMf_5icHdI-v+Q^QLO9eZHNXB2?!RV^N}M!6URVfD0kcdDA8^MWufzt7 zH93P=iGI9iyIZCUza&@N7H;iMAUQ2Oxjb3m3e)sAKt-G?0rfHIaSCo#Lms!a)zQ=n>y9$E^5FvRJta%0B`s|)IKLF*6JraQ^C)8ym3UdP?#PBTvcc#Q#1bR6-lbn7Dq8E7 z9%7=arEA*Zbkf+k>XKc+UNOaJmuK@xn{}_M%vPc1{A2N57;=lfBa^4l zXEgxTIJtQ^C@4{;qg?hO-}1GQCp|q+YMt@8DwMkk$vYVO$qi&vTMv6#d6M@)xhUu! zSfGg!tK10>H@#-WJ+bZ$+$3sth>o}o;<==vX_8&b?Rn9>WU(D}XnQzXe@b_?Ig~#j z>r5)hjHK^_fE~+C6R9DY=VNfiPR#|b)m{94%$di6{%;EI+Xw{)qT`%5Wqg9jJT3S1U(*kr!FIw4~K^>G9 zJLw=xqwN=?qmWtBhv`L448y08eAky;l7sB8Pc(bRmgGM{+~{$Vs;h-`87J@A1guaE zT-0MWtIcM~gsi7|M)_CXlZD&aO$Nn`9|7f&Bh;3o8$3?z99K?-f`v0|SwFKx{f^nc zYVO>SpQ_+;OM-U&N}vbSQBL*g1Ve}a3|IF0sDD=Bn;wt#Ypj`EX zl6_Rclb9bA@pIv*_QP0_QrCR};Ha$C1YgTeR|;-GEzU}@;n=F>_N~0P>*bk+`bvB1 zX6EXj45I8x9Uis2mI%BrqlC3lBf~avP36a9)9Y&q*2MVF7VcY@PWspLWulN_EVEQd zIK9{hzLWfJp5P5vUir<(QMTCFu5ythE0VJ#O|GfuENV&cjh{3kBiqAq=(sggiq#z+ z$f-0Yn&K_XyA>bWVvMk5LFig$6{|2_rSpm0?fo|QzGi#WCl1HOzp$+y`B7X(nOOv5 z!MJ_n@j%_B&e4N4e`@u~`*j6xYO3X@l+-+UENMc@d|%jq`KPtZLbt?fR{xDfm>edv zzx7gJtHMp21|vPgk}8cAeO$K@0~$D>LRK56`hTph)}TKD3O{HNT$MZM@)q#Fvvsh@ zJBI~J2nyTtZ?3j79QBv8>1gX+#d>KMHuBeRJN%%~Et75ZpSeukLc{ufn}feV1>CQs z&~@qY7C6)TlUh#mhjZRw#=YCk4SXW{vG}(kX){ZVxBx?858yxXRLbs zMIG^xlD(O6OJer9kHNy7*mO(l73b|Bp*OBgwFR};YtLoo=DN?AxlZ6ZUMdiTQlmXx zi9U{!NX7%AoY30pCBSh3) z7Vc|d>r)m0Gr(>XuOzDc3F6A{U_1LSZFFps*GrZx--)?dGUiaTeb4KFn5pC>{Hi-{ z^th?g&qySMiDAzz$#nZ9@vf`n-0pn%4cog$)tMzCzpPY|F3FBt+pzhI1Otn# zkK07}thWaa=4Zt@a5zjMUjQT_MK?Lyf- zAMVV`c}L#@3ZF<=ij0bJR6e-DhilG+JZ~=4(-~jctFY_S+|y$?W!83w;@>O1rr?7g z^^uZWc}PY_Q{Pv@ys}0bWhTku@IMXh@y}@u*Ms%zPKz;Sy@`Bd68iVQ47$@}vG z71I}?!US~z`6(#=K#QCdj zZc)}E5C3r|30*+JAUju@bt<5qxRi)y!8n)XI(l(0vzUDJ{seaP*bw$S$ev>dVAF$L zRO(r33eDwlwM8jKC_PscNqnF3?H?um`(`d)ky8R#z8^l}{D>1&^-!O!n^Eh+KO{>I zglu>p(Dv9J>xz7;vUGgWQtVc`R9nK(1J?X@BjRs>oO8$EI_rK=Xa(JoSJ;*>93#pr zI2ZXN-kc7MoSYki2hyya4%D*htp1=-Ifa7O$hr;(r_|=B3g)3J@BV!u{b!!#Z46M; zH1!R{?nv*=k{Pm*yVMp0oo+ep67tAH@^;h3fC8L1)k1MxV{#EU=e+J_S`@Sz$kO1( zfV{PSS!Yz9Oa2ACBYVaO*VOMX=~_pOHi04KyUjEY9yEor0@Ir;NQiZIkuz3X;d$7kD1ckpcckcJxTEHp~N13 zStyD;m!=CHyq-AtxiR#}t7qY^%E>*jJ)1 zM<2%VxHl5;23xGt;7C3ZaRtm{ZwFnQOFxs}a0KMNRPhu555diA5_6>@lil2x--nIj zPaiMSC4V1RFt;06?0U7Fe51i9tOE`hd^M7;UsLmwxDhPO~4DW?JSINzi$Sa#zha`47rU>}9 zo+Y!`8|6&9sksTjNq3ce&pggOf?Oc+FIllza^qptKGEpo3XdC4W*4IE=L*bpEnNw8 z%{xRRwN^j;i0Sy-6BBJg`)T{7QXvZls>(!(tpPZOEyenu-4cKO_ZI?xA@COheI)Tc!}U(LgjRAL7QbMs!_yJ^Xn2bqzmD zW;zwzv+;nJ(u}TR(OyP=1=_30h63DtOMnq{)+CbeFnXiHAfkTajaZ0sQE@jl5p>r4RJ4hK&G(eW_Y^GIebHZ`Tn4AiK~y@8TXhwtARo{rRQ($LukPBiAv z{G1L<(t)p<0TVk#$3;TZ^o#0t6q2z>aESblCODSbX1O)ccL2Ghl-f<|#;0V+Tzs4! z6D`=fFXzRDHR9lJ|Cw~Hr9hOyv$P6YctGY~U3VU9&V7W|i=8u{}_@t-rel!y~U?}5}H4yF9vv0eWD_cpp&`mAb#QDo?bM87;!emY6eBwAC z%+7|+bW8e`0w-r#zVn+AmJgbv&KB_tx#NbY=v%e*aJf`Z6nzEq^RI~hggm>dz;%|s z07T|5pt=OG)t)eVyOztA!~Sxu_M0;rsJ`b(w;yuuxEO96T3mn_R22$u8qo-BXF@ce znC%)n)lfe6j7~S7@oe#TAK587$W#U97ERUyp_WzQZci&WQU=+lhOKn5n3e|@%j8;K z)#a(gw@2C?n5z$#<{P7xGXCA_^}qFJJf&Bmn@jSj>O1Q5u|%WLWgF~p!CpB#nnSZs zRoco15hgWbWnnsH$|0g;-p4=>7p#Sruw?y|Bo?BOY-X${wkQ}Y=h2fjHbe^ECMG2l z$EEA~Wt^tHoW*P@ljQ9Iq4zNz&R4^Al{C&3WE(w+3SBVz8q?=>Gc--{4U6#KZ9D%K z8V>C@XoWNn2ACWR);)&~z`Nr|7H-V>i-81H=3a4QuTBeSHt_VAjULtp`bw|s*PLb^ zHePV_rgs~m<<$53m{uq)qUtR1sN zm2J|Fu)^Ruh}?|MOu2sjqh41{CZ68AWv%V~MZAuqWHJSjs8^KbXY8c)4|u8FLmVP; z!@}Rc4pT%P6l}U^;Qpe|&$T})+lkKq)a3}8(l zZ9Y%iXg==h8Q-^SoL>C}MBk}83yf1$oJWl-(?#@1m07P*t3lbpFSeXh3q9)OSoLF? zQjYp{!kGc@{a@KL|Nb);J5u%)k+z2~4}^KsJDD@`0xxuJgZ1GrV0kT}qFKPPeDSI3 zuvj$R(=WDnbsC!aJ69ytlh!M;{!zqlMg9Ax=L%T6yzOd7&+f=S;pd(`xxM6eQs%gH zs>V+&hHFRXIbuqQIS7X7(@u`f(9;G(GpnYl1;bhW5Hc~_Z>XH(Q)qYa@Gw1gobn`T z=<)68+-y86ub)%Hn0?x}-|3b}b|bNrA$>#l@6e~Dkirk)Q727nu{EQ=74RRBjtDLa zl}v8^prD+)FD7>7BtFNjo=K_bKzb$L9y0j0Wqd1W{KdlVUQzAwwL=H;LMXoyN%D%h zi=y{`1Lqu{RlBVk*#6jI>OfP`?WlRBL%XS}Nn)0u`7{C885=4@hNoy0rzh7QrzsE2x%mV6F1>(VXU@;Xq>&w~+N)pzw-r zoCr&wT}J~|Q$S+~`_W*HjoO~!Dm=xdYwJsPFv+ydA&YzldNvDc(ijX$#`f?^EPM_?-Xp_8q zQic+W0zbO*lJw$8Om}zS8&}L3cc-RAT{M@D)|^3o*rr>{hYHs!%c(}Kc#doh{i&lS z{@g*{3_xPum{P>t@fYM{CYs;8_)kck6PEOF>8Sy^KER;>i&bjX>{jTW_?g1c9Xi00 z2TK!7NRY97V&oC}UXmfv9GFj)mX2-2ysN85# z4O-*8emVbQ*ndJz{qCcv48ifcLED-6aZTXQe2uNDOFOc;NtYA6={NB85|3{k-o|;z zBN(qclzOpvb9U}GFe-7o_gcjh;%(-~GgB|UG2mX72yWFnqMqEYmq@x-V!cG%vKmV= zEx8o5Vlv}NQuE`pKtQ*2-z;%16-`LD#ZPx>*C)>^PJpS@9GA_m|E&X}kjLiA&tI3s z?+kbm%q9gGuXVVp4oJjF0g^IKZ10|W#BwHlS2$|i(|J%X3gvHA*&~)a;PRwih+Bej zNhIHzK(3}5w61XLz-J>tA)xOZ$EWRE87ltQXRT0eXX$U|WmsG)s_bK8QcUu{jPEx_ zw16!z>l>@9j~;C-=noGAMmwA?S|}@rj*8&hnYw`hFD_>&>RqgKzQ_{Bu%SGl^ zS%1c~IM#HA(WbVXM$w^E$Bbqlhzt!uRk4mf0Ki>#oB6}!F_BkBrbi8n7IwiB!hQAh z_cZd`)9I#d8F~Q^y2s4|!-U9WrejdW$ zdhtKuCj86C{{aC{=IYO{{b;RPXW>9qjgUQyxWjb&Zmn`@hc>UPG(rk_Jif;sJcvJVX;uw>O^iew63Suf=J?uBpQHwtFAb*0U6;&yJZB` zcd-tfp>Xr?9~8wjUloWT{~r|1H>K7UGOfa9LfmyPEUF?O9Y-XOZiA$Mvaq?<-e5`d z5$#f{pYyUZnXuh1WvL6%EV;Xn`r?<|gkRaUsHy8&YgHxPGmGcs{OPrqJYd&UMoM_R zAReo`4w8_@N}N^As+$2Addv+2i_m0&z;kiZDJk-gKpS>tG~IJ7-bN2w9>{v_3ykz1 zC?g&`4Cqa4e^fAO&yC4d*0ylt-Ul$Dnk115FNDv_ek8D03b1eXL~&GRqz2z7wt+l+GFfy?w9p!&JcB{PzPMm><`g`40-~xJmDN zz%bPCn~7ud&-A+$!+l^Wom!Dxr1tGnesv8n>fPW#tU?;}XGV$bn*rXtOBNip|73O^8()a zVkfG3PcQ`4p4r&xj#lihH<(Qt-G7-+?OF2d$Crm$q2kEx>3U8c`R&Y29YO33=}8k+nEgD7pedLrE;Ow z(L$%t!)S~*V-HAp5?@G*&vP^j`i0ChY#53)EB1?MyO8PztVvw5Y;9&U*fc7R;c_rdT+*Tw(Dd?xq9l0i5rYDg)W7@D2(emwknRZrX~?j3i;D^ux)b6-n@Dc)a?$>8ScX zY$iG1_&H3)Oj~%}#4_j;)9SY~L!a&?4#qkKrskh^?fR zz$0SC1PIo06%l#USZK32KTTA4Z?bP0i0C~zS8%p;EEOR5X0tzdCAsoshQviXw<#-; zwZ{@29)yxl!%)77EdRP(#iR{3cR1|xA34dLfs9gVcq00n+MW1%Kw(Foix5q>r01?O zGrV@o&FysV&I5uD09Y zI^u3anw4fp&1_tUoz64a?jH9ZoI{@L>BUYS#N9oI4en`PjDpdPv#j?qcc6&~v_(g_ z0Foem13|AL>2NTma8K4mlT7^eW+X->1=tBJH}#!9f$6R}|jPhRE$D*MZ zHQV zSN-@V)zx_X@|(n^7~mi{C!G*}xN;TkX0{gMaF_hT#xYQZzWaaG7Je(&Q1IB-TN{gR zHT2v9L%jqFb6&tY&`Q>gS`yj7#!kh}hueWRMKnofYM@Ju1@hBg!*F+qUwkl53QYrf z66t+VfloiRBY#Ppyjn%1> zG>WV7w*r&^=ZZRxA|GuqD~p)ugPP}(2&wF<1Z6zkRmHE%&YBR2|i9g zF7l;mEJvAsw?b}lg$?oRAZ-i08H(3iSoB?%e4`!o@`uYmK-cQj_Ox@R#L-MWDws)`J_>&716ql{X>iSDmTFa(t%2KRH4k8?{rs!5VVZ^0R_x1C@sho8S z1)3UHgm0ZG*DM%7P9HHX>~i7X90%+w926JIhMx_aD%*bSsTtrd8XIT+gxTc)B8Ca_ z2{pV&bgNOkc6$6v+4~k&2v=bt9#%?`Yr(atH;Zsztu5TPK&62SfGls

s>!$a%fYXVqbrgIt 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%0D~Jzy6y< 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 Date: Fri, 29 May 2026 19:39:04 +0900 Subject: [PATCH 94/97] Fix GFMC_t apc test under mpirun -np > 2. Compare global mean across ranks (allgather) instead of per-rank local mean because the production and debug functions yield different cums at the digit level. --- tests/test_jqmc_gfmc_tau.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_jqmc_gfmc_tau.py b/tests/test_jqmc_gfmc_tau.py index 9cf99358..f3705900 100755 --- a/tests/test_jqmc_gfmc_tau.py +++ b/tests/test_jqmc_gfmc_tau.py @@ -208,12 +208,19 @@ def test_jqmc_gfmc_t(trexio_file, with_1b_jastrow, with_2b_jastrow, with_3b_jast np.testing.assert_allclose(e_L2_debug, e_L2_jax, atol=atol, rtol=rtol) # average_projection_counter - # Both GFMC_t and _GFMC_t_debug now store local averages per rank. - apc_debug = gfmc_debug.average_projection_counter - apc_jax = gfmc_jax.average_projection_counter - assert not np.any(np.isnan(np.asarray(apc_debug))), "NaN detected in first argument" - assert not np.any(np.isnan(np.asarray(apc_jax))), "NaN detected in second argument" - np.testing.assert_allclose(apc_debug, apc_jax, atol=atol, rtol=rtol) + # Both GFMC_t and _GFMC_t_debug store local averages per rank. Production + # builds the branching cumprob via MPI allreduce + Exscan offset; debug + # uses a centralized rank-0 cumsum. The two paths are mathematically + # equivalent but not bit-identical, so boundary cases of searchsorted can + # permute walkers across ranks. Per-rank local apc is sensitive to that + # shuffling; the global mean across ranks is not. + apc_debug = np.asarray(gfmc_debug.average_projection_counter) + apc_jax = np.asarray(gfmc_jax.average_projection_counter) + assert not np.any(np.isnan(apc_debug)), "NaN detected in first argument" + assert not np.any(np.isnan(apc_jax)), "NaN detected in second argument" + apc_debug_global = np.mean(np.stack(mpi_comm.allgather(apc_debug), axis=0), axis=0) + apc_jax_global = np.mean(np.stack(mpi_comm.allgather(apc_jax), axis=0), axis=0) + np.testing.assert_allclose(apc_debug_global, apc_jax_global, atol=atol, rtol=rtol) # E E_debug, E_err_debug, Var_debug, Var_err_debug = gfmc_debug.get_E( From bd4e7815ef0a942264cd86f2bbaf5e349acc6059 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Fri, 29 May 2026 23:06:52 +0900 Subject: [PATCH 95/97] Update changelog.md --- doc/changelog.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/doc/changelog.md b/doc/changelog.md index 3c2fbd57..2de9f256 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,61 @@ # Change Log +## May-29-2026: v0.2.2 + +First stable release since v0.1.0. v0.2.2 ships everything accumulated across four alphas (v0.2.0a1, v0.2.1a1, v0.2.1a2, v0.2.2a1) plus a final round of polish. Per-alpha sections are preserved below; this entry is a roll-up of the highlights from v0.1.0 to v0.2.2. + +### Highlights (v0.1.0 -> v0.2.2) + +#### Optimization + +* **Linear Method (LM) optimizer** integrated under `method="sr"` with a unified `use_lm` / `lm_subspace_dim` hierarchy (plain SR / aSR / LM). New `|v_0|^2 < 0.9` fallback to plain SR keeps non-linear-regime updates from producing NaN energies. +* **Adaptive learning rate** for Stochastic Reconfiguration. +* **MO optimization** for JSD via the projection method with Attacalite-Sorella regularization, plus geminal AO -> MO projection. +* **AO basis optimization** (`opt_J3_basis_coeff/exp`, `opt_lambda_basis_coeff/exp`) with shell-shared constraint and dual symmetrization. +* **Distributed tall-CG SR** solver via `psum`, removing `mpi_size`-scaling memory in the SR solve. + +#### Performance + +* **Fast-update use** across MCMC / VMC / LRDMC, with mat-vec hot paths converted to GEMM for better GPU utilization. +* **On-GPU VMC optimization** with `use_device_collectives` auto-selected by JAX backend; multi-GPU `run_optimize` supported. +* **LU -> SVD** in determinant / geminal / GFMC_n / GFMC_t for ill-conditioned stability; Cartesian / Spherical AO conversion (Cartesian GTOs are substantially faster on GPU); ECP fast path (`compute_ecp_coulomb_potential_fast`). + +#### Numerical precision + +* **Mixed-precision support** with `"full"` / `"mixed"` modes and per-zone dtype control. Three explicit design principles. AGP/SD geminal stays fp64 to prevent `log|det|` amplification; electron-nucleus `r - R` differences are reconstructed in fp64 before downcast to avoid catastrophic cancellation. `ao_grad_lap` and `mo_grad_lap` zones are split for finer-grained control. + +#### Features + +* **LRDMC atomic forces** with the Pathak-Wagner regularization. +* **Runtime-selectable Jastrow forms**: `jastrow_1b_type` and `jastrow_2b_type` (`exp` / `pade`). +* **`use_swct` flag** to toggle Space Warp Coordinate Transformation in MCMC and GFMC_n / GFMC_t. + +#### `jqmc_workflow` automation package + +* **jqmc-workflow** is introduced as a multi-stage QMC pipeline orchestrator (WF conversion -> VMC opt -> MCMC / LRDMC production) with automatic step estimation, checkpointing, and remote job management. + +#### bug fixes + +* GFMC_n / GFMC_t spin-polarized (`n_up != n_dn`, `n_dn >= 1`) MPI bug. +* MPI deadlock in `max_time` / `stop_flag` checks; `Allreduce` vs `allreduce` for scalars. +* Optimizer step estimation; force NaN; MCMC memory overflow from `r_up_history` / `r_dn_history` storage. + +#### Infrastructure + +* **Restart files** migrated from pickle `.chk` to HDF5 `.h5` (no backward compatibility). +* **Ruff lint pipeline** (`jqmc-lint-ruff.yml`) and pre-commit updates; non-ASCII cleanup across code and docstrings. +* **Nightly CI + Codecov** activated with the `pytest-xdist` support. +* **Examples**: 11 end-to-end tutorials (`jqmc-example01` to `jqmc-example08`, `jqmc-workflow-example01` to `jqmc-workflow-example03`). +* **Project ownership** transferred to the `jqmc-project` GitHub organization; URLs updated. + +### Breaking changes since v0.1.0 + +* Restart files: pickle `.chk` is no longer supported; HDF5 `.h5` is the only format. +* Optimizer API: `num_param_opt`, `opt_filter_min_SN_ratio`, `adaptive_learning_rate`, and `method="lm"` are all removed or replaced; the Linear Method is now accessed via `method="sr"` with `use_lm=true` (and the new `lm_subspace_dim` / `lm_cond` parameters). + +See the per-alpha sections below for full details. + ## May-18-2026: v0.2.2a1 This release brings configurable mixed-precision support, deep kernel-level performance work (AOs, Jastrow, det/Jastrow ratios, GFMC), on-GPU VMC optimization, and a project-wide lint/cleanup. From a2e869bd637ad5c3a7701516141eba0275c9d0ef Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sat, 30 May 2026 09:02:52 +0900 Subject: [PATCH 96/97] Update jqmc-run-full-pytest-ubuntu.yml timeout-minutes 720 -> 1440 --- .github/workflows/jqmc-run-full-pytest-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml index 2344e61d..8b85030a 100644 --- a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml +++ b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml @@ -23,7 +23,7 @@ jobs: matrix: python-version: ["3.10.14", "3.11.9", "3.12.3"] - timeout-minutes: 720 + timeout-minutes: 1440 steps: - name: Show runner information From c2fadf171edfbf23f0bd04640821e9e7b91e0a56 Mon Sep 17 00:00:00 2001 From: kousuke-nakano <37653569+kousuke-nakano@users.noreply.github.com> Date: Sat, 30 May 2026 22:14:31 +0900 Subject: [PATCH 97/97] Cap GFMC mpirun tests at np=2: debug (centralized cumsum) vs production (allreduce+Exscan) cumprob is not bit-identical, and at fp32+np>2 the gap crosses searchsorted boundaries and permutes walkers across ranks, breaking the rank-local w_L/e_L/e_L2 compare (physics is still validated via get_E/get_aF MPI-aggregated checks). --- .github/workflows/jqmc-run-full-pytest-ubuntu.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml index 2344e61d..7abd921a 100644 --- a/.github/workflows/jqmc-run-full-pytest-ubuntu.yml +++ b/.github/workflows/jqmc-run-full-pytest-ubuntu.yml @@ -110,15 +110,15 @@ jobs: - name: Test jqmc FP64 (QMC kernels with 2MPIs, FP64) run: | - mpirun -np 8 pytest -v tests/test_jqmc_mcmc.py - mpirun -np 8 pytest -v tests/test_jqmc_gfmc_tau.py - mpirun -np 8 pytest -v tests/test_jqmc_gfmc_bra.py + mpirun -np 2 pytest -v tests/test_jqmc_mcmc.py + mpirun -np 2 pytest -v tests/test_jqmc_gfmc_tau.py + mpirun -np 2 pytest -v tests/test_jqmc_gfmc_bra.py - name: Test jqmc FP32+FP64 (QMC kernels with 2MPIs, FP32+FP64) run: | - mpirun -np 8 pytest -v tests/test_jqmc_mcmc.py --precision-mode=mixed - mpirun -np 8 pytest -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed - mpirun -np 8 pytest -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed + mpirun -np 2 pytest -v tests/test_jqmc_mcmc.py --precision-mode=mixed + mpirun -np 2 pytest -v tests/test_jqmc_gfmc_tau.py --precision-mode=mixed + mpirun -np 2 pytest -v tests/test_jqmc_gfmc_bra.py --precision-mode=mixed - name: Test jqmc-tool (Toolset for jqmc, FP64) run: |

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�M(uE;s-8SSlXy@+^1n?p3bkzqu8SzBy8+2 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 z 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