# Source code for netket.hilbert.discrete_hilbert

```# Copyright 2021 The NetKet Authors - All rights reserved.
#
# 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
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and

from typing import List, Tuple, Optional, Union, Iterator
from textwrap import dedent
from functools import reduce

import numpy as np

from netket.utils.types import Array

from .abstract_hilbert import AbstractHilbert

max_states = np.iinfo(np.int32).max
"""int: Maximum number of states that can be indexed"""

def _is_indexable(shape):
"""
Returns whether a discrete Hilbert space of shape `shape` is
indexable (i.e., its total number of states is below the maximum).
"""
log_max = np.log(max_states)
return np.sum(np.log(shape)) <= log_max

class DiscreteHilbert(AbstractHilbert):
"""Abstract class for an hilbert space defined on a lattice.

This class defines the common interface that can be used to
interact with hilbert spaces on lattices.
"""

[docs]    def __init__(self, shape: Tuple[int, ...]):
"""
Initializes a discrete Hilbert space with a basis of given shape.

Args:
shape: The local dimension of the Hilbert space for each degree
of freedom.
"""
self._shape = tuple(shape)

super().__init__()

@property
def shape(self) -> Tuple[int, ...]:
r"""The size of the hilbert space on every site."""
return self._shape

@property
def is_finite(self) -> bool:
r"""Whether the local hilbert space is finite."""
raise NotImplementedError(  # pragma: no cover
dedent(
f"""
`is_finite` is not implemented for discrete hilbert
space {type(self)}.
"""
)
)

@property
def n_states(self) -> int:
r"""The total dimension of the many-body Hilbert space.
Throws an exception iff the space is not indexable."""
raise NotImplementedError(  # pragma: no cover
dedent(
f"""
`n_states` is not implemented for discrete hilbert
space {type(self)}.
"""
)
)

[docs]    def size_at_index(self, i: int) -> int:
r"""Size of the local degrees of freedom for the i-th variable.

Args:
i: The index of the desired site

Returns:
The number of degrees of freedom at that site
"""
return self.shape[i]  # pragma: no cover

[docs]    def states_at_index(self, i: int) -> Optional[List[float]]:
r"""A list of discrete local quantum numbers at the site i.
If the local states are infinitely many, None is returned.

Args:
i: The index of the desired site.

Returns:
A list of values or None if there are infinitely many.
"""
raise NotImplementedError()  # pragma: no cover

[docs]    def numbers_to_states(
self, numbers: Union[int, np.ndarray], out: Optional[np.ndarray] = None
) -> np.ndarray:
r"""Returns the quantum numbers corresponding to the n-th basis state
for input n. n is an array of integer indices such that
:code:`numbers[k]=Index(states[k])`.
Throws an exception iff the space is not indexable.

Args:
numbers (numpy.array): Batch of input numbers to be converted into arrays of
quantum numbers.
out: Optional Array of quantum numbers corresponding to numbers.
"""
if out is None:
out = np.empty((np.atleast_1d(numbers).shape, self.size))

if np.any(numbers >= self.n_states):
raise ValueError("numbers outside the range of allowed states")

if np.isscalar(numbers):
return self._numbers_to_states(np.atleast_1d(numbers), out=out)[0, :]
else:
return self._numbers_to_states(numbers, out=out)

[docs]    def states_to_numbers(
self, states: np.ndarray, out: Optional[np.ndarray] = None
) -> Union[int, np.ndarray]:
r"""Returns the basis state number corresponding to given quantum states.
The states are given in a batch, such that states[k] has shape (hilbert.size).
Throws an exception iff the space is not indexable.

Args:
states: Batch of states to be converted into the corresponding integers.
out: Array of integers such that out[k]=Index(states[k]).
If None, memory is allocated.

Returns:
numpy.darray: Array of integers corresponding to out.
"""
if states.shape[-1] != self.size:
raise ValueError(
f"Size of this state ({states.shape[-1]}) not"
f"corresponding to this hilbert space {self.size}"
)

states_r = np.asarray(np.reshape(states, (-1, states.shape[-1])))

if out is None:
out = np.empty(states_r.shape[:-1], dtype=np.int64)

out = self._states_to_numbers(states_r, out=out.reshape(-1))

if states.ndim == 1:
return out
else:
return out.reshape(states.shape[:-1])

[docs]    def states(self) -> Iterator[np.ndarray]:
r"""Returns an iterator over all valid configurations of the Hilbert space.
Throws an exception iff the space is not indexable.
Iterating over all states with this method is typically inefficient,
and ```all_states``` should be preferred.

"""
for i in range(self.n_states):
yield self.numbers_to_states(i).reshape(-1)

[docs]    def all_states(self, out: Optional[np.ndarray] = None) -> np.ndarray:
r"""Returns all valid states of the Hilbert space.

Throws an exception if the space is not indexable.

Args:
out: an optional pre-allocated output array

Returns:
A (n_states x size) batch of states. this corresponds
to the pre-allocated array if it was passed.
"""
numbers = np.arange(0, self.n_states, dtype=np.int64)

return self.numbers_to_states(numbers, out)

[docs]    def states_to_local_indices(self, x: Array):
r"""Returns a tensor with the same shape of `x`, where all local
values are converted to indices in the range `0...self.shape[i]`.
This function is guaranteed to be jax-jittable.

For the `Fock` space this returns `x`, but for other hilbert spaces
such as `Spin` this returns an array of indices.

NOTE: This function is experimental. Use at your own risk.

Args:
x: a tensor containing samples from this hilbert space

Returns:
a tensor containing integer indices into the local hilbert
"""
raise NotImplementedError(
"states_to_local_indices(self, x) is not "
f"implemented for Hilbert space {self} of type {type(self)}"
)

@property
def is_indexable(self) -> bool:
"""Whether the space can be indexed with an integer"""
if not self.is_finite:
return False
return _is_indexable(self.shape)

def __mul__(self, other: "DiscreteHilbert"):
if type(self) == type(other):
res = self._mul_sametype_(other)
if res is not NotImplemented:
return res

if isinstance(other, DiscreteHilbert):
from .tensor_hilbert_discrete import TensorDiscreteHilbert

return TensorDiscreteHilbert(self, other)
elif isinstance(other, AbstractHilbert):
from .tensor_hilbert import TensorGenericHilbert

return TensorGenericHilbert(self, other)

return NotImplemented

def __pow__(self, n):
return reduce(lambda x, y: x * y, [self for _ in range(n)])
```