Source code for ffsim.states.states

# (C) Copyright IBM 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Fermionic quantum states."""

from __future__ import annotations

import math
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Optional, cast

import numpy as np
from pyscf.fci.spin_op import contract_ss

from ffsim.states.bitstring import (
    BitstringType,
    addresses_to_strings,
    concatenate_bitstrings,
    restrict_bitstrings,
)


[docs] @dataclass class StateVector: """A state vector in the FCI representation. Attributes: vec: Array of state vector coefficients. norb: The number of spatial orbitals. nelec: Either a single integer representing the number of fermions for a spinless system, or a pair of integers storing the numbers of spin alpha and spin beta fermions. """ vec: np.ndarray norb: int nelec: int | tuple[int, int] def __array__(self, dtype=None, copy=None): # TODO in Numpy 2.0 this can be simplified to # return np.array(self.vec, dtype=dtype, copy=copy) if copy: if dtype is None: return self.vec.copy() else: return self.vec.astype(dtype, copy=True) if dtype is None: return self.vec return self.vec.astype(dtype, copy=False)
[docs] def dims(norb: int, nelec: tuple[int, int]) -> tuple[int, int]: """Get the dimensions of the FCI space. Args: norb: The number of spatial orbitals. nelec: The number of alpha and beta electrons. Returns: A pair of integers (dim_a, dim_b) representing the dimensions of the alpha- and beta- FCI space. """ n_alpha, n_beta = nelec dim_a = math.comb(norb, n_alpha) dim_b = math.comb(norb, n_beta) return dim_a, dim_b
[docs] def dim(norb: int, nelec: int | tuple[int, int]) -> int: """Get the dimension of the FCI space. Args: norb: The number of spatial orbitals. nelec: Either a single integer representing the number of fermions for a spinless system, or a pair of integers storing the numbers of spin alpha and spin beta fermions. Returns: The dimension of the FCI space. """ if isinstance(nelec, int): return math.comb(norb, nelec) n_alpha, n_beta = nelec return math.comb(norb, n_alpha) * math.comb(norb, n_beta)
# source: pyscf.fci.spin_op.spin_square0 # modified to support complex wavefunction
[docs] def spin_square(fcivec: np.ndarray, norb: int, nelec: tuple[int, int]): """Expectation value of spin squared operator on a state vector.""" if np.iscomplexobj(fcivec): ci1 = contract_ss(fcivec.real, norb, nelec).astype(complex) ci1 += 1j * contract_ss(fcivec.imag, norb, nelec) else: ci1 = contract_ss(fcivec, norb, nelec) return np.einsum("ij,ij->", fcivec.reshape(ci1.shape), ci1.conj()).real
[docs] def sample_state_vector( vec: np.ndarray | StateVector, *, norb: int | None = None, nelec: int | tuple[int, int] | None = None, orbs: Sequence[int] | tuple[Sequence[int], Sequence[int]] | None = None, shots: int = 1, concatenate: bool = True, bitstring_type: BitstringType = BitstringType.STRING, seed: np.random.Generator | int | None = None, ): """Sample bitstrings from a state vector. Args: vec: The state vector to sample from. norb: The number of spatial orbitals. nelec: Either a single integer representing the number of fermions for a spinless system, or a pair of integers storing the numbers of spin alpha and spin beta fermions. orbs: The spin-orbitals to sample. In the spinless case (when `nelec` is an integer), this is a list of integers ranging from ``0`` to ``norb``. In the spinful case, this is a pair of lists of such integers, with the first list storing the spin-alpha orbitals and the second list storing the spin-beta orbitals. If not specified, then all spin-orbitals are sampled. shots: The number of bitstrings to sample. concatenate: Whether to concatenate the spin-alpha and spin-beta parts of the bitstrings. If True, then a single list of concatenated bitstrings is returned. The strings are concatenated in the order :math:`s_b s_a`, that is, the alpha string appears on the right. If False, then two lists are returned, ``(strings_a, strings_b)``. Note that the list of alpha strings appears first, that is, on the left. In the spinless case (when `nelec` is an integer), this argument is ignored. bitstring_type: The desired type of bitstring output. seed: A seed to initialize the pseudorandom number generator. Should be a valid input to ``np.random.default_rng``. Returns: The sampled bitstrings, as a list of strings of length `shots`. Raises: TypeError: When passing vec as a Numpy array, norb and nelec must be specified. TypeError: When passing vec as a StateVector, norb and nelec must both be None. """ vec, norb, nelec = canonicalize_vec_norb_nelec(vec, norb, nelec) if isinstance(nelec, int): return _sample_state_vector_spinless( vec, norb=norb, nelec=nelec, orbs=cast(Optional[Sequence[int]], orbs), shots=shots, bitstring_type=bitstring_type, seed=seed, ) return _sample_state_vector_spinful( vec, norb=norb, nelec=nelec, orbs=cast(Optional[tuple[Sequence[int], Sequence[int]]], orbs), shots=shots, concatenate=concatenate, bitstring_type=bitstring_type, seed=seed, )
def _sample_state_vector_spinless( vec: np.ndarray, *, norb: int, nelec: int, orbs: Sequence[int] | None, shots: int, bitstring_type: BitstringType, seed: np.random.Generator | int | None, ): if orbs is None: orbs = range(norb) rng = np.random.default_rng(seed) probabilities = np.abs(vec) ** 2 samples = rng.choice(len(vec), size=shots, p=probabilities) strings = addresses_to_strings(samples, norb, nelec, bitstring_type=bitstring_type) if list(orbs) == list(range(norb)): return strings return restrict_bitstrings(strings, orbs, bitstring_type=bitstring_type) def _sample_state_vector_spinful( vec: np.ndarray, *, norb: int, nelec: tuple[int, int], orbs: tuple[Sequence[int], Sequence[int]] | None, shots: int, concatenate: bool = True, bitstring_type: BitstringType, seed: np.random.Generator | int | None, ): if orbs is None: orbs = range(norb), range(norb) rng = np.random.default_rng(seed) probabilities = np.abs(vec) ** 2 samples = rng.choice(len(vec), size=shots, p=probabilities) orbs_a, orbs_b = orbs if list(orbs_a) == list(orbs_b) == list(range(norb)): # All orbitals are sampled, so we can simply call addresses_to_strings return addresses_to_strings( samples, norb, nelec, concatenate=concatenate, bitstring_type=bitstring_type ) strings_a, strings_b = addresses_to_strings( samples, norb, nelec, concatenate=False, bitstring_type=bitstring_type ) strings_a = restrict_bitstrings(strings_a, orbs_a, bitstring_type=bitstring_type) strings_b = restrict_bitstrings(strings_b, orbs_b, bitstring_type=bitstring_type) if concatenate: return concatenate_bitstrings( strings_a, strings_b, bitstring_type=bitstring_type, length=len(orbs_a) ) return strings_a, strings_b def canonicalize_vec_norb_nelec( vec: np.ndarray | StateVector, norb: int | None, nelec: int | tuple[int, int] | None ) -> tuple[np.ndarray, int, int | tuple[int, int]]: """Canonicalize a state vector, number of orbitals, and number(s) of electrons. Args: vec: The state vector. norb: The number of spatial orbitals, or None if `vec` is a StateVector. nelec: Either a single integer representing the number of fermions for a spinless system, or a pair of integers storing the numbers of spin alpha and spin beta fermions. If `vec` is a StateVector then this should be None. Returns: The state vector as a Numpy array, the number of spatial orbitals, and the number or numbers of electrons. Raises: TypeError: When passing vec as a Numpy array, norb and nelec must be specified. TypeError: When passing vec as a StateVector, norb and nelec must both be None. """ if isinstance(vec, np.ndarray): if norb is None or nelec is None: raise TypeError( "When passing vec as a Numpy array, norb and nelec must be specified." ) else: if norb is not None or nelec is not None: raise TypeError( "When passing vec as a StateVector, norb and nelec must both be None." ) norb = vec.norb nelec = vec.nelec vec = vec.vec return vec, norb, nelec