Source code for netket.experimental.hilbert.spin_orbital_fermions

# Copyright 2022-2023 The NetKet Authors - All rights reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Optional
from import Iterable
import warnings

import numpy as np
from fractions import Fraction

from netket.hilbert.fock import Fock
from netket.hilbert.tensor_hilbert_discrete import TensorDiscreteHilbert
from netket.hilbert.homogeneous import HomogeneousHilbert
from netket.utils.dispatch import dispatch
from netket.utils import StaticRange

[docs] class SpinOrbitalFermions(HomogeneousHilbert): r""" Hilbert space for 2nd quantization fermions with spin `s` distributed among `n_orbital` orbitals. The number of fermions can be fixed globally or fixed on a per spin projection. Note: This class is simply a convenient wrapper that creates a Fock or TensorHilbert of Fock spaces with occupation numbers 0 or 1. It is mainly useful to avoid needing to specify the n_max=1 each time, and adds convenient functions such as _get_index and _spin_index, which allow one to index the correct TensorHilbert corresponding to the right spin projection. """
[docs] def __init__( self, n_orbitals: int, s: Optional[float] = None, *, n_fermions: Optional[int] = None, n_fermions_per_spin: Optional[tuple[int, ...]] = None, ): r""" Constructs the hilbert space for spin-`s` fermions on `n_orbitals`. Samples of this hilbert space represent occupation numbers :math:`(0,1)` of the orbitals. The number of fermions may be fixed to `n_fermions`. If the spin is different from 0 or None, n_fermions can also be a list to fix the number of fermions per spin component. Using this class, one can generate a tensor product of fermionic hilbert spaces that distinguish particles with different spin. Args: n_orbitals: number of orbitals we store occupation numbers for. If the number of fermions per spin is conserved, the different spin configurations are not counted as orbitals and are handled differently. s: spin of the fermions. n_fermions: (optional) fixed number of fermions per spin (conserved). If specified, the total number of fermions is conserved. If the fermions have a spin, the number of fermions per spin subsector is not conserved. n_fermions_per_spin: (optional) list of the fixed number of fermions for every spin subsector. This automatically enforces a global population constraint. Only one between **n_fermions** and **n_fermions_per_spin** can be specified. The length of the iterable should be :math:`2S+1`. Returns: A SpinOrbitalFermions object """ # TODO remove in 3.12 because it's deprecated experimental behaviour if isinstance(n_fermions, Iterable): warnings.warn( """ Declaring per-spin subsector constraint with `n_fermions` is deprecated. Use `n_fermions_per_spin` instead. As this class was experimental, this behaviour will break in NetKet 3.12 to be released in early 2024. """, DeprecationWarning, stacklevel=2, ) n_fermions_per_spin = n_fermions n_fermions = None if not (isinstance(n_fermions, int) or n_fermions is None): raise TypeError( """ The global `n_fermions` constraint must be an integer or be left unspecified. To declare a per-spin constraint, specify `n_fermions_per_spin` instead """ ) if s is None: spin_size = 1 else: spin_size = round(2 * s + 1) total_size = n_orbitals * spin_size if spin_size == 1: if n_fermions_per_spin is not None: raise ValueError( "Cannot specify `n_fermions_per_spin` for spin-less " "fermions." ) n_fermions_per_spin = (n_fermions,) hilbert = Fock(n_max=1, N=n_orbitals, n_particles=n_fermions) else: if n_fermions_per_spin is not None and n_fermions is not None: raise ValueError( """ Cannot specify the per-subsector constraint `n_fermions_per_spin` at the same time as the global constraint `n_fermions`. If you want to conserve the total number of fermions, but allow for changes in the number of fermions with a certain value of spin, you should specify `n_fermions`. If you want to fix the number of fermions with each spin component (and therefore the total number of fermions as well) you should specify `n_fermions_per_spin`. """ ) elif n_fermions_per_spin is not None: if not isinstance(n_fermions_per_spin, Iterable): raise TypeError( f"n_fermions_per_spin={n_fermions_per_spin} (whose type is {type(n_fermions_per_spin)}) " "must be a list of integers or None describing the number of " f"fermions in each of the {total_size} spin subsectors." ) if len(n_fermions_per_spin) != spin_size: raise ValueError( "List of number of fermions must equal number of spin components.\n" f"For s={s}, which has {total_size} components, the same length is " f"expected." ) n_fermions = sum(n_fermions_per_spin) spin_hilberts = [ Fock(n_max=1, N=n_orbitals, n_particles=Nf) for Nf in n_fermions_per_spin ] hilbert = TensorDiscreteHilbert(*spin_hilberts) else: # fixed fermion number but multiple spins subspaces # global or no constraint n_fermions_per_spin = tuple(None for _ in range(spin_size)) hilbert = Fock(n_max=1, N=total_size, n_particles=n_fermions) self._fock = hilbert """Internal representation of this Hilbert space (Fock or TensorHilbert).""" # local states are the occupation numbers (0, 1) local_states = StaticRange(0.0, 1.0, 2, dtype=np.int8) # we use the constraints from the Fock spaces, and override `constrained` super().__init__(local_states, N=total_size, constraint_fn=None) self._s = s self._n_fermions = n_fermions self._n_fermions_per_subsector: tuple[Optional[int], ...] = n_fermions_per_spin self._n_orbitals = n_orbitals
@property def _numbers_to_states(self): return self._fock._numbers_to_states @property def _states_to_numbers(self): return self._fock._states_to_numbers @property def all_states(self): return self._fock.all_states @property def n_fermions(self) -> Optional[int]: """The total number of fermions. None if unspecified.""" return self._n_fermions @property def n_fermions_per_spin(self) -> tuple[Optional[int], ...]: """Tuple identifying the per-subsector population constraint. This tuple has length 1 for spinless fermions and length 2s+1 for spinful fermions. Every element is an integer or None, where None means no constraint on the number of fermions in that subsector. """ return self._n_fermions_per_subsector @property def n_spin_subsectors(self) -> int: """Total number of spin subsectors. If spin is None, this is 1.""" return len(self._n_fermions_per_subsector) @property def n_orbitals(self) -> int: """The number of orbitals (for each spin subsector if relevant).""" return self._n_orbitals @property def spin(self) -> float: """Returns the spin of the fermions""" return self._s @property def size(self) -> int: """Size of the hilbert space. In case the fermions have spin `s`, the size is (2*s+1)*n_orbitals""" return self._fock.size @property def _attrs(self): return (self.spin, self.n_fermions, self.n_orbitals) @property def constrained(self): r"""The hilbert space does not contains `prod(hilbert.shape)` basis states. Typical constraints are population constraints (such as fixed number of bosons, fixed magnetization...) which ensure that only a subset of the total unconstrained space is populated. """ return self._n_fermions is not None @property def is_finite(self) -> bool: """True if the hilbert space can be indexed by a single 32-bit unsigned integer. """ return self._fock.is_finite @property def n_states(self) -> int: """ Total size of the hilbert space, if indexable. """ return self._fock.n_states @property def _n_spin_states(self) -> int: """Return the number of spin projections""" if self.spin is None: raise Exception( "cannot request number of spin states for spinless fermions" ) return round(2 * self.spin + 1) def _spin_index(self, sz: Optional[int]) -> int: """Return the index of the Fock block corresponding to the sz projection""" if self.spin is None: if sz is not None or not np.isclose(sz, 0): raise Exception("cannot request spin index of spinless fermions") return 0 else: if sz != int(sz): raise TypeError(f"sz must be an integer, but got {type(sz)} ({sz})") if abs(sz) > 2 * self.spin: val = int(2 * self.spin) raise ValueError( f"Valid spin values are in the interval [{-val},{val}]" ) # if integer spin, valid values are even if int(self.spin) == self.spin: if sz % 2 != 0: raise ValueError( f"For spin S={self.spin}, valid spin values are odd" ) else: # valid values are odd if sz % 2 == 0: raise ValueError( f"For spin S={self.spin}, valid spin values are even" ) return (sz + int(2 * self.spin)) // 2
[docs] def states_to_local_indices(self, x): return self._fock.states_to_local_indices(x)
def _get_index(self, orb: int, sz: Optional[float] = None): """go from (site, spin_projection) indices to index in the hilbert space""" if orb >= self.n_orbitals: raise IndexError("requested orbital index outside of the hilbert space") spin_idx = self._spin_index(sz) return spin_idx * self.n_orbitals + orb def __repr__(self): _str = f"SpinOrbitalFermions(n_orbitals={self.n_orbitals}" if self.spin is not None: _str += f", s={Fraction(self.spin)}" if self.n_fermions is not None: _str += f", n_fermions={self.n_fermions}" if self.spin is not None and any( x is not None for x in self.n_fermions_per_spin ): _str += f", n_fermions_per_spin={self.n_fermions_per_spin}" _str += ")" return _str @property def is_indexable(self) -> bool: """Whether the space can be indexed with an integer""" return self._fock.is_indexable
@dispatch def random_state(hilb: SpinOrbitalFermions, key, batches: int, *, dtype): return random_state(hilb._fock, key, batches, dtype) @dispatch def flip_state_scalar(hilb: SpinOrbitalFermions, key, state, index): return flip_state_scalar(hilb._fock, key, state, index) @dispatch def flip_state_batch(hilb: SpinOrbitalFermions, key, state, index): return flip_state_batch(hilb._fock, key, state, index)