Source code for aimspy.matrix

"""Public — AimspyMatrix + aims↔aimspy format conversions.

The aimspy standard matrix format is a block-sparse real-space
representation:

    blocks: dict[tuple[int, int, int, int, int], np.ndarray]
           key = (R1, R2, R3, i_atom, j_atom)

Conventions
-----------
- *R*: ``R_aimspy = -R_aims`` (same sign as DeepH).
- *Atoms*: aims native order (no reordering).
- *Orbitals*: aims native basis order (no reordering).
- *Parity*: wiki/DeepH convention (``phase_i * phase_j`` already applied).
- *Units*: Hartree.
- *Hermitian partners*: both ``(R,i,j)`` and ``(-R,j,i)`` stored.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, Optional, Tuple

import numpy as np

from .data import CsrMatrixDescriptor
from .structure import AimspyStructure


# =============================================================================
# Helper: pointer to ndarray copy  (unchanged from earlier version)
# =============================================================================
def _ptr_to_ndarray(ptr, shape, dtype=np.float64) -> np.ndarray:
    from ctypes import cast, c_void_p, POINTER, c_double as _cd

    n = 1
    for d in shape:
        n *= d
    try:
        flat = np.ctypeslib.as_array(ptr, shape=(n,))
    except Exception:
        flat = np.ctypeslib.as_array(cast(c_void_p(ptr), POINTER(_cd)), shape=(n,))
    return np.ascontiguousarray(flat.reshape(shape), dtype=dtype).copy()


# =============================================================================
# Accessors — read Fortran arrays through ctypes  (unchanged)
# =============================================================================
def get_rs_hamiltonian(binding, n_spin: int, n_ham_size: int) -> np.ndarray:
    from ._exceptions import AimspyBindingError

    ptr = binding.c_rs_hamiltonian()
    if not ptr:
        raise AimspyBindingError("c_rs_hamiltonian() returned NULL")
    return _ptr_to_ndarray(ptr, (n_spin, n_ham_size))


def get_rs_overlap(binding, n_ham_size: int) -> np.ndarray:
    from ._exceptions import AimspyBindingError

    ptr = binding.c_rs_overlap()
    if not ptr:
        raise AimspyBindingError("c_rs_overlap() returned NULL")
    return _ptr_to_ndarray(ptr, (n_ham_size,))


[docs] def get_forces(binding, n_atoms: int) -> Optional[np.ndarray]: """Read total_forces (3, n_atoms) Fortran array → (n_atoms, 3) eV/Å. Fortran stores total_forces in Hartree/Bohr; we convert to eV/Å (the same convention FHI-aims uses for printed forces in aims.out). Returns None if use_forces=False (Fortran returns c_null_ptr when `compute_forces .true.` was not set in control.in). """ from .data import HARTREE_TO_EV, BOHR_TO_ANG ptr = binding.aimspy_forces() if not ptr: return None # use_forces=False — forces not computed # Fortran (3, n_atoms) column-major → (n_atoms, 3) raw = _ptr_to_ndarray(ptr, (n_atoms, 3)) # Hartree/Bohr → eV/Å return raw * (HARTREE_TO_EV / BOHR_TO_ANG)
# ============================================================================= # AimspyMatrix — canonical block-sparse matrix in aimspy standard format # =============================================================================
[docs] @dataclass class AimspyMatrix: """Block-sparse real-space matrix in aimspy standard format. Key = ``(R1, R2, R3, i_atom, j_atom)`` with all ints: - R follows ``R_aimspy = -R_aims`` (same sign as DeepH). - i_atom / j_atom in aims native order. - Orbital order within each atom is aims native. - Parity = wiki/DeepH (phase already applied). - Units = Hartree. """ blocks: Dict[Tuple[int, ...], np.ndarray] # key -> (n_orb_i, n_orb_j) n_spin: int = 1 # ---------------------------------------------------------------- # aims CSR ↔ aimspy # ----------------------------------------------------------------
[docs] @classmethod def from_aims_csr( cls, h0: np.ndarray, # (n_spin, n_ham_size), C-contiguous csr_descr: CsrMatrixDescriptor, structure: AimspyStructure, ) -> "AimspyMatrix": """Convert aims CSR flat array to aimspy block dict. Steps: 1. Walk CSR triplanes (cell, basis‑row, k‑index). 2. R_aimspy = -R_aims (sign flip) → lookup key matches DeepH. 3. Apply wiki parity: ``v *= phase_i * phase_j``. 4. Store block[orb_i, orb_j] and its Hermitian partner. Raises ------ AimspyError If ``csr_descr.n_spin != 1`` (spin-polarized data is not yet supported; only spin channel 0 would be read). """ if csr_descr.n_spin != 1: from ._exceptions import AimspyError raise AimspyError( f"from_aims_csr: spin-polarized data (n_spin=" f"{csr_descr.n_spin}) is not yet supported; n_spin=1 only" ) phase = structure.phase_factor subidx = structure.basis_subidx opa = structure.orbit_per_atom blocks: dict = {} n_cells_loop = csr_descr.n_cells - 1 # skip sentinel n_ham = csr_descr.n_ham_size for ic in range(n_cells_loop): R0 = -int(csr_descr.cell_idx[0, ic]) # R_aimspy = -R_aims R1 = -int(csr_descr.cell_idx[1, ic]) R2 = -int(csr_descr.cell_idx[2, ic]) for ib_row in range(csr_descr.n_basis): start = int(csr_descr.row_mx_idx[ib_row, ic, 0]) end = int(csr_descr.row_mx_idx[ib_row, ic, 1]) if start < 1 or end < start: continue atom_i = int(structure.basis_atom[ib_row]) orb_i = int(subidx[ib_row]) pi = int(phase[ib_row]) for k in range(start - 1, end): if k >= n_ham: continue # skip trash ib_col = int(csr_descr.col_mx_idx[k]) - 1 atom_j = int(structure.basis_atom[ib_col]) orb_j = int(subidx[ib_col]) pj = int(phase[ib_col]) key = (R0, R1, R2, atom_i, atom_j) rev_key = (-R0, -R1, -R2, atom_j, atom_i) if key not in blocks: blocks[key] = np.zeros( (opa[atom_i], opa[atom_j]), dtype=np.float64 ) if rev_key not in blocks: blocks[rev_key] = np.zeros( (opa[atom_j], opa[atom_i]), dtype=np.float64 ) v = h0[0, k] * pi * pj # apply parity blocks[key][orb_i, orb_j] = v # Hermitian partner: write if unset, else verify consistency. # CSR stores upper-triangle only, so the reverse entry # (j,i) at -R should already equal (i,j) at R. If it was # previously written (abs > 1e-12), check agreement within # 1e-11 — well above double round-off (~1e-13 for |v|~1e3). existing_rev = blocks[rev_key][orb_j, orb_i] if abs(existing_rev) <= 1e-12: blocks[rev_key][orb_j, orb_i] = v elif abs(existing_rev - v) > 1e-11: from ._exceptions import AimspyError raise AimspyError( f"Hermitian check failed at R=({R0},{R1},{R2}), " f"atom=({atom_i},{atom_j}), orb=({orb_i},{orb_j}): " f"existing={existing_rev:.6e}, new={v:.6e}" ) return cls(blocks=blocks, n_spin=int(h0.shape[0]))
[docs] def to_aims_csr( self, csr_descr: CsrMatrixDescriptor, structure: AimspyStructure, ) -> np.ndarray: """Convert aimspy block dict back to aims CSR flat array. Steps: 1. Walk CSR triplanes (same order as ``from_aims_csr``). 2. Look up block in ``self.blocks`` (dict, O(1)). 3. Hermitian fallback: if forward key missing, try ``(-R, j, i)``. 4. Undo parity: ``v *= phase_i * phase_j`` (self‑inverse). 5. Return ``(n_spin, n_ham_size)`` C‑contiguous, ready to memmove. Raises ------ AimspyError If ``csr_descr.n_spin != 1`` (spin-polarized data is not yet supported; only spin channel 0 would be written). """ if csr_descr.n_spin != 1: from ._exceptions import AimspyError raise AimspyError( f"to_aims_csr: spin-polarized data (n_spin=" f"{csr_descr.n_spin}) is not yet supported; n_spin=1 only" ) phase = structure.phase_factor subidx = structure.basis_subidx n_ham = csr_descr.n_ham_size n_spin = csr_descr.n_spin out = np.zeros((n_spin, n_ham), dtype=np.float64) n_cells_loop = csr_descr.n_cells - 1 for ic in range(n_cells_loop): R0 = -int(csr_descr.cell_idx[0, ic]) R1 = -int(csr_descr.cell_idx[1, ic]) R2 = -int(csr_descr.cell_idx[2, ic]) for ib_row in range(csr_descr.n_basis): start = int(csr_descr.row_mx_idx[ib_row, ic, 0]) end = int(csr_descr.row_mx_idx[ib_row, ic, 1]) if start < 1 or end < start: continue atom_i = int(structure.basis_atom[ib_row]) orb_i = int(subidx[ib_row]) pi = int(phase[ib_row]) for k in range(start - 1, end): if k >= n_ham: continue ib_col = int(csr_descr.col_mx_idx[k]) - 1 atom_j = int(structure.basis_atom[ib_col]) orb_j = int(subidx[ib_col]) pj = int(phase[ib_col]) key = (R0, R1, R2, atom_i, atom_j) blk = self.blocks.get(key) if blk is not None: if orb_i < blk.shape[0] and orb_j < blk.shape[1]: val = blk[orb_i, orb_j] else: val = 0.0 else: rev_key = (-R0, -R1, -R2, atom_j, atom_i) blk = self.blocks.get(rev_key) if ( blk is not None and orb_j < blk.shape[0] and orb_i < blk.shape[1] ): val = blk[orb_j, orb_i] # Hermitian fallback else: val = 0.0 val *= pi * pj # undo parity out[0, k] = val return out
@property def n_pairs(self) -> int: return len(self.blocks) def __repr__(self) -> str: return f"AimspyMatrix(n_pairs={self.n_pairs}, n_spin={self.n_spin})"