From 703d1fb4dc39c6859665297800b02e32ce269d08 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Thu, 15 Aug 2024 12:24:39 -0400 Subject: [PATCH 01/38] Lattice class and template spin Hamiltonians --- pennylane/__init__.py | 1 + pennylane/spin/__init__.py | 21 ++ pennylane/spin/lattice.py | 189 +++++++++++++++ pennylane/spin/lattice_shapes.py | 82 +++++++ pennylane/spin/spin_hamiltonian.py | 141 +++++++++++ pennylane/spin/utils.py | 80 ++++++ tests/spin/test_lattice.py | 378 +++++++++++++++++++++++++++++ 7 files changed, 892 insertions(+) create mode 100644 pennylane/spin/__init__.py create mode 100644 pennylane/spin/lattice.py create mode 100644 pennylane/spin/lattice_shapes.py create mode 100644 pennylane/spin/spin_hamiltonian.py create mode 100644 pennylane/spin/utils.py create mode 100644 tests/spin/test_lattice.py diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 3f0c484fc4f..323b243e529 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -153,6 +153,7 @@ from pennylane.devices.device_constructor import device, refresh_devices +import pennylane.spin # Look for an existing configuration file default_config = Configuration("config.toml") diff --git a/pennylane/spin/__init__.py b/pennylane/spin/__init__.py new file mode 100644 index 00000000000..2e8af385e7a --- /dev/null +++ b/pennylane/spin/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. + +# 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 + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This submodule provides the functionality to obtain spin Hamiltonians. +""" + +from .lattice import Lattice +from .lattice_shapes import Chain, Square, Rectangle, Honeycomb, Triangle +from .utils import map_vertices, get_custom_edges +from .spin_hamiltonian import generate_lattice, transverse_ising, heisenberg, fermihubbard diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py new file mode 100644 index 00000000000..37ad6f9dc45 --- /dev/null +++ b/pennylane/spin/lattice.py @@ -0,0 +1,189 @@ +import numpy as np +import scipy +from scipy.spatial import cKDTree +from scipy.sparse import find, triu +from .utils import map_vertices, get_custom_edges + + +class Lattice: + def __init__( + self, + L, + unit_cell, + basis=None, + boundary_condition=False, + neighbour_order=1, + custom_edges=None, + distance_tol=1e-5, + ): + r"""Constructs a Lattice object. + Args: + L: Number of unit cells in a direction, it is a list depending on the dimensions of the lattice. + unit_cell: Primitive vectors for the lattice. + basis: Initial positions of spins. + boundary_conditions: Boundary conditions, boolean or series of bools with dimensions same as L. + neighbour_order: Range of neighbours a spin interacts with. + custom_edges: List of edges in a unit cell along with the operations associated with them. + distance_tol: Distance below which spatial points are considered equal for the purpose of identifying nearest neighbours. + """ + + self.L = np.asarray(L) + self.n_dim = len(L) + self.unit_cell = np.asarray(unit_cell) + if basis is None: + basis = np.zeros(self.unit_cell.shape[0])[None, :] + self.basis = np.asarray(basis) + self.n_sl = len(self.basis) + self.n_sites = np.prod(L) * self.n_sl + + if isinstance(boundary_condition, bool): + boundary_condition = [boundary_condition for _ in range(self.n_dim)] + + self.boundary_condition = boundary_condition + self.custom_edges = custom_edges + self.test_input_accuracy() + + if True in self.boundary_condition: + extra_shells = np.where(self.boundary_condition, neighbour_order, 0) + else: + extra_shells = None + + self.coords, self.sl_coords, self.lattice_points = self.generate_grid(extra_shells) + + if self.custom_edges is None: + cutoff = neighbour_order * np.linalg.norm(self.unit_cell, axis=1).max() + distance_tol + self.identify_neighbours(cutoff, neighbour_order) + self.generate_true_edges(neighbour_order) + else: + if neighbour_order > 1: + raise ValueError( + "custom_edges and neighbour_order cannot be specified at the same time" + ) + self.edges = get_custom_edges( + self.unit_cell, + self.L, + self.basis, + self.boundary_condition, + distance_tol, + self.lattice_points, + self.n_sites, + self.custom_edges, + ) + + def test_input_accuracy(self): + for l in self.L: + if (not isinstance(l, np.int64)) or l <= 0: + raise TypeError("Argument `L` must be a list of positive integers") + + if self.unit_cell.ndim != 2: + raise ValueError("'unit_cell' must have ndim==2, as array of primitive vectors.") + + if self.basis.ndim != 2: + raise ValueError("'basis' must have ndim==2, as array of initial coordinates.") + + if self.unit_cell.shape[0] != self.unit_cell.shape[1]: + raise ValueError("The number of primitive vectors must match their length") + + if not all(isinstance(b, bool) for b in self.boundary_condition): + raise ValueError( + "Argument 'boundary_condition' must be a bool or a list of bools of same dimensions as the unit_cell" + ) + + if len(self.boundary_condition) != self.n_dim: + raise ValueError( + "Argument 'boundary_condition' must be a bool or a list of bools of same dimensions as the unit_cell" + ) + + def identify_neighbours(self, cutoff, neighbour_order): + r"""Identifies the connections between lattice points and returns the unique connections + based on the neighbour_order""" + + tree = cKDTree(self.lattice_points) + indices = tree.query_ball_tree(tree, cutoff) + unique_pairs = set() + row = [] + col = [] + distance = [] + for i, neighbours in enumerate(indices): + for neighbour in neighbours: + if neighbour != i: + pair = (min(i, neighbour), max(i, neighbour)) + if pair not in unique_pairs: + unique_pairs.add(pair) + dist = np.linalg.norm( + self.lattice_points[i] - self.lattice_points[neighbour] + ) + row.append(i) + col.append(neighbour) + distance.append(dist) + + row = np.array(row) + col = np.array(col) + + # Sort distance into bins for comparison + bin_density = 21621600 # multiple of expected denominators + distance = np.asarray(np.rint(np.asarray(distance) * bin_density), dtype=int) + + _, ii = np.unique(distance, return_inverse=True) + + self.edges = [sorted(list(zip(row[ii == k], col[ii == k]))) for k in range(neighbour_order)] + + def generate_true_edges(self, neighbour_order): + map = map_vertices(self.coords, self.sl_coords, self.L, self.basis) + colored_edges = [] + for k, edge in enumerate(self.edges): + true_edges = set() + for node1, node2 in edge: + node1 = map[node1] + node2 = map[node2] + if node1 == node2: + raise RuntimeError(f"Lattice contains self-referential edge {(node1, node2)}.") + true_edges.add((min(node1, node2), max(node1, node2))) + for edge in true_edges: + colored_edges.append((*edge, k)) + self.edges = colored_edges + + def add_edge(self, edge_index): + if len(edge_index) == 2: + edge_index = (*edge_index, 0) + + self.edges.append(edge_index) + + def generate_grid(self, extra_shells): + """Generates the coordinates of all lattice sites. + + Args: + extra_shells (np.ndarray): Optional. The number of unit cells added along each lattice direction. + This is used for near-neighbour searching in periodic boundary conditions (PBC). + It must be a vector of the same length as L. + + Returns: + basis_coords: The coordinates of the basis sites in each unit cell. + sl_coords: The coordinates of sublattice sites in each lattice site. + lattice_points: The coordinates of all lattice sites. + """ + + # Initialize extra_shells if not provided + if extra_shells is None: + extra_shells = np.zeros(self.L.size, dtype=int) + + shell_min = -extra_shells + shell_max = self.L + extra_shells + + range_dim = [] + for i in range(self.n_dim): + range_dim.append(np.arange(shell_min[i], shell_max[i])) + + range_dim.append(np.arange(0, self.n_sl)) + + coords = np.meshgrid(*range_dim, indexing="ij") + + sl_coords = coords[-1].ravel() + basis_coords = np.column_stack([c.ravel() for c in coords[:-1]]) + + lattice_points = (np.dot(basis_coords, self.unit_cell)).astype(float) + + for i in range(0, len(lattice_points), self.n_sl): + lattice_points[i : i + self.n_sl] = lattice_points[i : i + self.n_sl] + self.basis + + return basis_coords, sl_coords, lattice_points diff --git a/pennylane/spin/lattice_shapes.py b/pennylane/spin/lattice_shapes.py new file mode 100644 index 00000000000..66de91915ff --- /dev/null +++ b/pennylane/spin/lattice_shapes.py @@ -0,0 +1,82 @@ +from pennylane import numpy as np +from .lattice import Lattice + + +def Chain(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a chain lattice""" + unit_cell = [[1]] + L = n_cells[0:1] + lattice_chain = Lattice( + L, + unit_cell=unit_cell, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + + +def Square(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a square lattice""" + unit_cell = [[1, 0], [0, 1]] + basis = [[0, 0]] + + L = n_cells[0:2] + lattice_square = Lattice( + L=L, + unit_cell=unit_cell, + basis=basis, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + + return lattice_square + + +def Rectangle(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a rectangle lattice""" + unit_cell = [[1, 0], [0, 1]] + basis = [[0, 0]] + + L = n_cells[0:2] + lattice_rec = Lattice( + L=L, + unit_cell=unit_cell, + basis=basis, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + + return lattice_rec + + +def Honeycomb(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a honeycomb lattice""" + unit_cell = [[1, 0], [0.5, np.sqrt(3) / 2]] + basis = [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]] + + L = n_cells[0:2] + lattice_honeycomb = Lattice( + L=L, + unit_cell=unit_cell, + basis=basis, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + + return lattice_honeycomb + + +def Triangle(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a triangular lattice""" + unit_cell = [[1, 0], [0.5, np.sqrt(3) / 2]] + basis = [[0, 0]] + + L = n_cells[0:2] + lattice_triangle = Lattice( + L=L, + unit_cell=unit_cell, + basis=basis, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + + return lattice_triangle diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py new file mode 100644 index 00000000000..a90cdf3a001 --- /dev/null +++ b/pennylane/spin/spin_hamiltonian.py @@ -0,0 +1,141 @@ +import pennylane as qml +from pennylane import numpy as np +from pennylane import X, Y, Z +from pennylane.fermi import FermiWord + +from .lattice import Lattice +from .lattice_shapes import * + + +def generate_lattice(lattice, n_cells, boundary_condition, neighbour_order): + + lattice = lattice.strip().lower() + + if lattice not in ["chain", "square", "rectangle", "honeycomb", "triangle"]: + raise ValueError( + f"Lattice shape, '{lattice}' is not supported." + f"Please set lattice to: chain, square, rectangle, honeycomb, or triangle" + ) + + if lattice == "chain": + lattice = Chain(n_cells, boundary_condition, neighbour_order) + elif lattice == "square": + lattice = Square(n_cells, boundary_condition, neighbour_order) + elif lattice == "rectangle": + lattice = Rectangle(n_cells, boundary_condition, neighbour_order) + elif lattice == "honeycomb": + lattice = Honeycomb(n_cells, boundary_condition, neighbour_order) + elif lattice == "triangle": + lattice = Triangle(n_cells, boundary_condition, neighbour_order) + + return lattice + + +def transverse_ising( + lattice, n_cells, coupling, h=1.0, boundary_condition=False, neighbour_order=1 +): + r"""Generates the transverse field Ising model on a lattice. + The Hamiltonian is represented as: + .. math:: + + \hat{H} = -J \sum_{} \sigma_i^{z} \sigma_j^{z} - h\sum{i} \sigma_{i}^{x} + + where J is the coupling defined for the Hamiltonian, h is the strength of transverse + magnetic field and i,j represent the indices for neighbouring spins. + """ + + lattice = generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) + hamiltonian = 0.0 + if isinstance(coupling, (int, float, complex)): + for edge in lattice.edges: + i, j = edge[0], edge[1] + hamiltonian += -coupling * (Z(i) @ Z(j)) + else: + for edge in lattice.edges: + i, j = edge[0], edge[1] + hamiltonian += -coupling[i][j] * (Z(i) @ Z(j)) + + for vertex in range(lattice.n_sites): + hamiltonian += -h * X(vertex) + + return hamiltonian + + +def heisenberg(lattice, n_cells, coupling, boundary_condition=False, neighbour_order=1): + r"""Generates the Heisenberg model on a lattice. + The Hamiltonian is represented as: + .. math:: + + \hat{H} = J\sum_{}(\sigma_i^x\sigma_j^x + \sigma_i^y\sigma_j^y + \sigma_i^z\sigma_j^z) + + where J is the coupling constant defined for the Hamiltonian, and i,j represent the indices for neighbouring spins. + """ + + lattice = generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) + + hamiltonian = 0.0 + if isinstance(coupling[0], (int, float, complex)): + for edge in lattice.edges: + i, j = edge[0], edge[1] + hamiltonian += ( + coupling[0] * X(i) @ X(j) + coupling[1] * Y(i) @ Y(j) + coupling[2] * Z(i) @ Z(j) + ) + else: + for edge in lattice.edges: + i, j = edge[0], edge[1] + hamiltonian += ( + coupling[0][i][j] * X(i) @ X(j) + + coupling[1][i][j] * Y(i) @ Y(j) + + coupling[2][i][j] * Z(i) @ Z(j) + ) + + return hamiltonian + + +def fermihubbard( + lattice, + n_cells, + hopping, + interaction, + boundary_condition=False, + neighbour_order=1, + mapping="jordan_wigner", +): + r"""Generates the Hubbard model on a lattice. + The Hamiltonian is represented as: + .. math:: + + \hat{H} = -t\sum_{, \sigma}(c_{i\sigma}^{\dagger}c_{j\sigma}) + U\sum_{i}n_{i \uparrow} n_{i\downarrow} + + where t is the hopping term representing the kinetic energy of electrons, and U is the on-site Coulomb interaction, representing the repulsion between electrons. + """ + lattice = generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) + + hamiltonian = 0.0 + if isinstance(hopping, (int, float, complex)): + for edge in lattice.edges: + i, j = edge[0], edge[1] + hopping_term = -hopping * ( + FermiWord({(0, i): "+", (1, j): "-"}) + FermiWord({(0, j): "+", (1, i): "-"}) + ) + int_term = interaction * FermiWord({(0, i): "+", (1, i): "-", (2, j): "+", (3, j): "-"}) + hamiltonian += hopping_term + int_term + else: + for edge in lattice.edges: + i, j = edge[0], edge[1] + hopping_term = -hopping[i][j] * ( + FermiWord({(0, i): "+", (1, j): "-"}) + FermiWord({(0, j): "+", (1, i): "-"}) + ) + int_term = interaction[i][j] * FermiWord( + {(0, i): "+", (1, i): "-", (2, j): "+", (3, j): "-"} + ) + hamiltonian += hopping_term + int_term + + if mapping not in ["jordan_wigner", "parity", "bravyi_kitaev"]: + raise ValueError( + f"The '{mapping}' transformation is not available." + f"Please set mapping to 'jordan_wigner', 'parity', or 'bravyi_kitaev'" + ) + qubit_ham = qml.qchem.qubit_observable(hamiltonian, mapping=mapping) + + return qubit_ham.simplify() diff --git a/pennylane/spin/utils.py b/pennylane/spin/utils.py new file mode 100644 index 00000000000..12a44d589b7 --- /dev/null +++ b/pennylane/spin/utils.py @@ -0,0 +1,80 @@ +import numpy as np + + +def map_vertices(basis_coords, sl, L, basis): + """Generates lattice site indices for unit cell + sublattice coordinates.""" + + basis_coords = basis_coords % L + + site_indices = np.zeros(basis_coords.shape[0], dtype=int) + + num_sl = len(basis) + num_dim = len(L) + + nsites_axis = np.zeros(num_dim, dtype=int) + nsites_axis[-1] = num_sl + + for j in range(num_dim - 1, 0, -1): + nsites_axis[j - 1] = nsites_axis[j] * L[num_dim - j] + + for index in range(basis_coords.shape[0]): + site_indices[index] = np.dot(basis_coords[index], nsites_axis) + site_indices += sl + + return site_indices + + +def get_custom_edges( + unit_cell, L, basis, boundary_condition, atol, lattice_points, n_sites, custom_edges +): + """Generates the edges described in `custom_edges` for all unit cells.""" + + if not all([len(edge) in (1, 2) for edge in custom_edges]): + raise TypeError( + """ + custom_edges must be a list of tuples of length 1 or 2. + Every tuple must contain two lattice indices to represent the edge + and can optionally include a list to represent the operation and coefficient for that edge. + """ + ) + + def define_custom_edges(edge): + if edge[0] >= n_sites or edge[1] >= n_sites: + raise ValueError(f"The edge {edge} has vertices greater than n_sites, {n_sites}") + num_sl = len(basis) + sl1 = edge[0] % num_sl + sl2 = edge[1] % num_sl + new_coords = lattice_points[edge[1]] - lattice_points[edge[0]] + return (sl1, sl2, new_coords) + + def translated_edges(sl1, sl2, distance, operation): + # get distance in terms of unit cells, each axis represents the difference between two points in terms of unit_cells + d_cell = (distance + basis[sl1] - basis[sl2]) @ np.linalg.inv(unit_cell) + d_cell = np.asarray(np.rint(d_cell), dtype=int) + + # Unit cells of starting points + edge_ranges = [] + for i in range(len(L)): + if boundary_condition[i]: + edge_ranges.append(np.arange(0, L[i])) + else: + edge_ranges.append( + np.arange(np.maximum(0, -d_cell[i]), L[i] - np.maximum(0, d_cell[i])) + ) + + start_grid = np.meshgrid(*edge_ranges, indexing="ij") + start_grid = np.column_stack([g.ravel() for g in start_grid]) + end_grid = (start_grid + d_cell) % L + + # Convert to site indices + edge_start = map_vertices(start_grid, sl1, L, basis) + edge_end = map_vertices(end_grid, sl2, L, basis) + return [(*edge, operation) for edge in zip(edge_start, edge_end)] + + edges = [] + for i, custom_edge in enumerate(custom_edges): + edge = custom_edge[0] + edge_operation = custom_edge[1] if len(custom_edge) == 2 else i + edge_data = define_custom_edges(edge) + edges += translated_edges(*edge_data, edge_operation) + return edges diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py new file mode 100644 index 00000000000..1cdbc72b285 --- /dev/null +++ b/tests/spin/test_lattice.py @@ -0,0 +1,378 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. + +# 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 + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests for functions needed for computing the lattice. +""" +import pytest + +import pennylane as qml +from pennylane import numpy as np +from pennylane.spin import Lattice + + +def test_boundary_condition_dimension_error(): + r"""Test that an error is raised if a wrong dimensions are entered for boundary_condition.""" + unit_cell = [[1]] + L = [10] + with pytest.raises(ValueError, match="Argument 'boundary_condition' must be a bool"): + lattice = Lattice(L=L, unit_cell=unit_cell, boundary_condition=[True, True]) + + +def test_boundary_condition_type_error(): + r"""Test that an error is raised if a wrong type is entered for boundary_condition.""" + unit_cell = [[1]] + L = [10] + with pytest.raises(ValueError, match="Argument 'boundary_condition' must be a bool"): + lattice = Lattice(L=L, unit_cell=unit_cell, boundary_condition=[4]) + + +def test_unit_cell_error(): + r"""Test that an error is raised if a wrong dimension is entered for unit_cell.""" + unit_cell = [0, 1] + L = [2, 2] + with pytest.raises( + ValueError, match="'unit_cell' must have ndim==2, as array of primitive vectors." + ): + lattice = Lattice(L=L, unit_cell=unit_cell) + + +def test_basis_error(): + r"""Test that an error is raised if a wrong dimension is entered for basis.""" + unit_cell = [[0, 1], [1, 0]] + L = [2, 2] + basis = [0, 0] + with pytest.raises( + ValueError, match="'basis' must have ndim==2, as array of initial coordinates." + ): + lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis) + + +def test_unit_cell_shape_error(): + r"""Test that an error is raised if a wrong dimension is entered for unit_cell.""" + unit_cell = [[0, 1, 2], [0, 1, 1]] + L = [2, 2] + with pytest.raises(ValueError, match="The number of primitive vectors must match their length"): + lattice = Lattice(L=L, unit_cell=unit_cell) + + +def test_L_error(): + r"""Test that an error is raised if length of unit_cell is provided in negative.""" + + unit_cell = [[0, 1], [1, 0]] + L = [2, -2] + with pytest.raises(TypeError, match="Argument `L` must be a list of positive integers"): + lattice = Lattice(L=L, unit_cell=unit_cell) + + +def test_L_type_error(): + r"""Test that an error is raised if length of unit_cell is provided not as an int.""" + + unit_cell = [[0, 1], [1, 0]] + L = [2, 2.4] + with pytest.raises(TypeError, match="Argument `L` must be a list of positive integers"): + lattice = Lattice(L=L, unit_cell=unit_cell) + + +def test_neighbour_order_error(): + r"""Test that an error is raised if neighbour order is greater than 1 when custom_edges are provided.""" + + unit_cell = [[0, 1], [1, 0]] + L = [3, 3] + custom_edges = [[(0, 1)], [(0, 5)], [(0, 4)]] + with pytest.raises( + ValueError, match="custom_edges and neighbour_order cannot be specified at the same time" + ): + lattice = Lattice(L=L, unit_cell=unit_cell, neighbour_order=2, custom_edges=custom_edges) + + +def test_custom_edge_type_error(): + r"""Test that an error is raised if custom_edges are not provided as a list of length 1 or 2.""" + + unit_cell = [[0, 1], [1, 0]] + L = [3, 3] + custom_edges = [[(0, 1), 1, 3], [(0, 5)], [(0, 4)]] + with pytest.raises(TypeError, match="custom_edges must be a list of tuples of length 1 or 2."): + lattice = Lattice(L=L, unit_cell=unit_cell, custom_edges=custom_edges) + + +def test_custom_edge_value_error(): + r"""Test that an error is raised if the custom_edges contains an edge with site_index greater than number of sites""" + + unit_cell = [[0, 1], [1, 0]] + L = [3, 3] + custom_edges = [[(0, 1)], [(0, 5)], [(0, 12)]] + with pytest.raises(ValueError, match="The edge \(0, 12\) has vertices greater than n_sites, 9"): + lattice = Lattice(L=L, unit_cell=unit_cell, custom_edges=custom_edges) + + +@pytest.mark.parametrize( + ("unit_cell", "basis", "L"), + [ + ([[0, 1], [1, 0]], [[1.5, 1.5]], [3, 3]), + ([[0, 1], [1, 0]], [[-1, -1]], [3, 3]), + ([[0, 1], [1, 0]], [[10, 10]], [3, 3]), + ([[1, 0], [0.5, np.sqrt(3) / 2]], [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], [2, 2]), + ], +) +def test_basis(unit_cell, basis, L): + r"""Test that the lattice points start from the coordinates provided in the basis""" + + lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis) + for i in range(len(basis)): + assert np.allclose(basis[i], lattice.lattice_points[i]) + + +@pytest.mark.parametrize( + # expected_edges here were obtained manually + ("unit_cell", "basis", "L", "custom_edges", "expected_edges"), + [ + ( + [[0, 1], [1, 0]], + [[0, 0]], + [3, 3], + [[(0, 1)], [(0, 5)], [(0, 4)]], + [(0, 1, 0), (0, 5, 1), (0, 4, 2), (1, 2, 0), (3, 4, 0), (0, 4, 2), (1, 5, 2)], + ), + ( + [[0, 1], [1, 0]], + [[0, 0]], + [3, 4], + [[(0, 1)], [(1, 4)], [(1, 5)]], + [(0, 1, 0), (1, 2, 0), (2, 3, 0), (1, 4, 1), (2, 5, 1), (0, 4, 2), (2, 6, 2)], + ), + ( + [[1, 0], [0.5, np.sqrt(3) / 2]], + [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], + [2, 2], + [[(0, 1)], [(1, 2)], [(1, 5)]], + [(2, 3, 0), (4, 5, 0), (1, 2, 1), (5, 6, 1), (1, 5, 2), (3, 7, 2)], + ), + ], +) +def test_custom_edges(unit_cell, basis, L, custom_edges, expected_edges): + r"""Test that the edges are added as per custom_edges provided""" + lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis, custom_edges=custom_edges) + assert np.all(np.isin(expected_edges, lattice.edges)) + + +@pytest.mark.parametrize( + ("unit_cell", "basis", "L", "expected_number"), + # expected_number here was obtained manually + [ + ([[0, 1], [1, 0]], [[0, 0]], [3, 3], 9), + ([[0, 1], [1, 0]], [[0, 0]], [6, 7], 42), + ([[1, 0], [0.5, np.sqrt(3) / 2]], [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], [2, 2], 8), + (np.eye(3), None, [3, 3, 4], 36), + ], +) +def test_lattice_points(unit_cell, basis, L, expected_number): + r"""Test that the correct number of lattice points are generated for the given attributes""" + lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis) + assert len(lattice.lattice_points == expected_number) + + +@pytest.mark.parametrize( + # expected_edges here were obtained with netket. + ("unit_cell", "basis", "L", "boundary_condition", "expected_edges"), + [ + ( + [[0, 1], [1, 0]], + [[0, 0]], + [3, 3], + [True, True], + [ + (0, 1, 0), + (1, 2, 0), + (3, 4, 0), + (5, 8, 0), + (6, 8, 0), + (0, 3, 0), + (1, 4, 0), + (0, 6, 0), + (4, 7, 0), + (6, 7, 0), + (1, 7, 0), + (0, 2, 0), + (4, 5, 0), + (3, 6, 0), + (2, 5, 0), + (3, 5, 0), + (7, 8, 0), + (2, 8, 0), + ], + ), + ( + [[0, 1], [1, 0]], + [[0, 0]], + [3, 4], + [True, False], + [ + (3, 7, 0), + (8, 9, 0), + (0, 8, 0), + (1, 9, 0), + (4, 5, 0), + (5, 6, 0), + (4, 8, 0), + (5, 9, 0), + (9, 10, 0), + (0, 1, 0), + (10, 11, 0), + (0, 4, 0), + (1, 2, 0), + (1, 5, 0), + (2, 10, 0), + (6, 7, 0), + (6, 10, 0), + (3, 11, 0), + (2, 3, 0), + (2, 6, 0), + (7, 11, 0), + ], + ), + ( + [[1, 0], [0.5, np.sqrt(3) / 2]], + [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], + [2, 2], + True, + [ + (0, 1, 0), + (1, 2, 0), + (2, 7, 0), + (0, 3, 0), + (1, 4, 0), + (2, 3, 0), + (6, 7, 0), + (4, 5, 0), + (5, 6, 0), + (0, 5, 0), + (3, 6, 0), + (4, 7, 0), + ], + ), + ], +) +def test_boundary_condition(unit_cell, basis, L, boundary_condition, expected_edges): + r"""Test that the correct edges are obtained for given boundary conditions""" + lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis, boundary_condition=boundary_condition) + assert lattice.edges == expected_edges + + +@pytest.mark.parametrize( + # expected_edges here were obtained with netket. + ("unit_cell", "basis", "L", "neighbour_order", "expected_edges"), + [ + ( + [[0, 1], [1, 0]], + [[0, 0]], + [3, 3], + 2, + [ + (0, 1, 0), + (1, 2, 0), + (3, 4, 0), + (5, 8, 0), + (0, 3, 0), + (1, 4, 0), + (6, 7, 0), + (4, 5, 0), + (3, 6, 0), + (2, 5, 0), + (4, 7, 0), + (7, 8, 0), + (2, 4, 1), + (0, 4, 1), + (1, 5, 1), + (3, 7, 1), + (4, 6, 1), + (5, 7, 1), + (4, 8, 1), + (1, 3, 1), + ], + ), + ( + [[0, 1], [1, 0]], + [[0, 0]], + [3, 4], + 3, + [ + (0, 1, 0), + (9, 10, 0), + (1, 2, 0), + (0, 4, 0), + (10, 11, 0), + (1, 5, 0), + (3, 7, 0), + (2, 3, 0), + (6, 7, 0), + (4, 5, 0), + (8, 9, 0), + (2, 6, 0), + (5, 6, 0), + (4, 8, 0), + (6, 10, 0), + (5, 9, 0), + (7, 11, 0), + (2, 7, 1), + (5, 8, 1), + (4, 9, 1), + (6, 11, 1), + (7, 10, 1), + (1, 4, 1), + (5, 10, 1), + (0, 5, 1), + (3, 6, 1), + (1, 6, 1), + (2, 5, 1), + (6, 9, 1), + (2, 10, 2), + (4, 6, 2), + (8, 10, 2), + (5, 7, 2), + (0, 2, 2), + (9, 11, 2), + (0, 8, 2), + (1, 3, 2), + (1, 9, 2), + (3, 11, 2), + ], + ), + ([[1, 0], [0.5, np.sqrt(3) / 2]], [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], [2, 2], 0, []), + ], +) +def test_neighbour_order(unit_cell, basis, L, neighbour_order, expected_edges): + r"""Test that the correct edges are obtained for given neighbour order""" + lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis, neighbour_order=neighbour_order) + assert lattice.edges == expected_edges + + +@pytest.mark.parametrize( + ("unit_cell", "basis", "L", "boundary_condition", "n_dim", "expected_bc"), + [ + ([[0, 1], [1, 0]], [[1.5, 1.5]], [3, 3], True, 2, [True, True]), + ([[0, 1], [1, 0]], [[-1, -1]], [3, 3], False, 2, [False, False]), + ([[0, 1], [1, 0]], [[10, 10]], [3, 3], [True, False], 2, [True, False]), + ([[1, 0], [0.5, np.sqrt(3) / 2]], [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], [2, 2], [True, True], 2, [True, True]), + (np.eye(3), [[0,0,0]], [3, 3, 4], True, 3, [True, True, True]), + ], +) + +def test_attributes(unit_cell, basis, L, boundary_condition, n_dim, expected_bc): + r"""Test that the methods and attributes return correct values""" + lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis,boundary_condition=boundary_condition) + + assert(np.all(lattice.unit_cell == unit_cell)) + assert(np.all(lattice.basis == basis)) + assert(lattice.n_dim == n_dim) + assert(np.all(lattice.boundary_condition == expected_bc)) From da3af9546dd6a3be85c526a624f658e2ead19d45 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Thu, 15 Aug 2024 13:12:15 -0400 Subject: [PATCH 02/38] Docstring formatting --- pennylane/spin/lattice.py | 64 ++++++++++++++++++-------- pennylane/spin/lattice_shapes.py | 19 ++++++++ pennylane/spin/spin_hamiltonian.py | 73 +++++++++++++++++++++++++----- pennylane/spin/utils.py | 28 ++++++++++-- 4 files changed, 151 insertions(+), 33 deletions(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 37ad6f9dc45..7f667feb3d8 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -1,11 +1,45 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. + +# 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 + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains functions and classes to create a +:class:`~pennylane.spin.lattice` object. This object stores all +the necessary information about a lattice. +""" + import numpy as np -import scipy from scipy.spatial import cKDTree -from scipy.sparse import find, triu from .utils import map_vertices, get_custom_edges +# pylint: disable=too-many-arguments, too-many-instance-attributes + class Lattice: + r"""Constructs a Lattice object. + + Args: + L: Number of unit cells in a direction, it is a list depending on the dimensions of the lattice. + unit_cell: Primitive vectors for the lattice. + basis: Initial positions of spins. + boundary_condition: defines boundary conditions, boolean or series of bools with dimensions same as L. + neighbour_order: Range of neighbours a spin interacts with. + custom_edges: List of edges in a unit cell along with the operations associated with them. + distance_tol: Distance below which spatial points are considered equal for the purpose of identifying nearest neighbours. + + Returns: + Lattice object + """ + def __init__( self, L, @@ -16,16 +50,6 @@ def __init__( custom_edges=None, distance_tol=1e-5, ): - r"""Constructs a Lattice object. - Args: - L: Number of unit cells in a direction, it is a list depending on the dimensions of the lattice. - unit_cell: Primitive vectors for the lattice. - basis: Initial positions of spins. - boundary_conditions: Boundary conditions, boolean or series of bools with dimensions same as L. - neighbour_order: Range of neighbours a spin interacts with. - custom_edges: List of edges in a unit cell along with the operations associated with them. - distance_tol: Distance below which spatial points are considered equal for the purpose of identifying nearest neighbours. - """ self.L = np.asarray(L) self.n_dim = len(L) @@ -40,7 +64,6 @@ def __init__( boundary_condition = [boundary_condition for _ in range(self.n_dim)] self.boundary_condition = boundary_condition - self.custom_edges = custom_edges self.test_input_accuracy() if True in self.boundary_condition: @@ -50,10 +73,10 @@ def __init__( self.coords, self.sl_coords, self.lattice_points = self.generate_grid(extra_shells) - if self.custom_edges is None: + if custom_edges is None: cutoff = neighbour_order * np.linalg.norm(self.unit_cell, axis=1).max() + distance_tol self.identify_neighbours(cutoff, neighbour_order) - self.generate_true_edges(neighbour_order) + self.generate_true_edges() else: if neighbour_order > 1: raise ValueError( @@ -64,13 +87,14 @@ def __init__( self.L, self.basis, self.boundary_condition, - distance_tol, self.lattice_points, self.n_sites, - self.custom_edges, + custom_edges, ) def test_input_accuracy(self): + r"""Tests the accuracy of the input provided""" + for l in self.L: if (not isinstance(l, np.int64)) or l <= 0: raise TypeError("Argument `L` must be a list of positive integers") @@ -128,7 +152,9 @@ def identify_neighbours(self, cutoff, neighbour_order): self.edges = [sorted(list(zip(row[ii == k], col[ii == k]))) for k in range(neighbour_order)] - def generate_true_edges(self, neighbour_order): + def generate_true_edges(self): + r"""Modifies the edges to remove hidden nodes and create connections based on boundary_conditions""" + map = map_vertices(self.coords, self.sl_coords, self.L, self.basis) colored_edges = [] for k, edge in enumerate(self.edges): @@ -144,6 +170,8 @@ def generate_true_edges(self, neighbour_order): self.edges = colored_edges def add_edge(self, edge_index): + r"""Adds a specific edge based on the site index without translating it""" + if len(edge_index) == 2: edge_index = (*edge_index, 0) diff --git a/pennylane/spin/lattice_shapes.py b/pennylane/spin/lattice_shapes.py index 66de91915ff..633db255007 100644 --- a/pennylane/spin/lattice_shapes.py +++ b/pennylane/spin/lattice_shapes.py @@ -1,3 +1,21 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. + +# 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 + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains functions to create +:class:`~pennylane.spin.lattice` objects of different shapes. +""" + from pennylane import numpy as np from .lattice import Lattice @@ -12,6 +30,7 @@ def Chain(n_cells, boundary_condition=False, neighbour_order=1): neighbour_order=neighbour_order, boundary_condition=boundary_condition, ) + return lattice_chain def Square(n_cells, boundary_condition=False, neighbour_order=1): diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index a90cdf3a001..193e3626226 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -1,31 +1,49 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. + +# 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 + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains functions to create different templates of spin Hamiltonians. +""" + import pennylane as qml -from pennylane import numpy as np from pennylane import X, Y, Z from pennylane.fermi import FermiWord -from .lattice import Lattice -from .lattice_shapes import * +from .lattice_shapes import Chain, Square, Rectangle, Honeycomb, Triangle + +# pylint: disable=too-many-arguments def generate_lattice(lattice, n_cells, boundary_condition, neighbour_order): + r"""Generates the lattice object for given attributes.""" - lattice = lattice.strip().lower() + lattice_shape = lattice.strip().lower() - if lattice not in ["chain", "square", "rectangle", "honeycomb", "triangle"]: + if lattice_shape not in ["chain", "square", "rectangle", "honeycomb", "triangle"]: raise ValueError( f"Lattice shape, '{lattice}' is not supported." f"Please set lattice to: chain, square, rectangle, honeycomb, or triangle" ) - if lattice == "chain": + if lattice_shape == "chain": lattice = Chain(n_cells, boundary_condition, neighbour_order) - elif lattice == "square": + elif lattice_shape == "square": lattice = Square(n_cells, boundary_condition, neighbour_order) - elif lattice == "rectangle": + elif lattice_shape == "rectangle": lattice = Rectangle(n_cells, boundary_condition, neighbour_order) - elif lattice == "honeycomb": - lattice = Honeycomb(n_cells, boundary_condition, neighbour_order) - elif lattice == "triangle": + elif lattice_shape == "honeycomb": + lattice_shape = Honeycomb(n_cells, boundary_condition, neighbour_order) + elif lattice_shape == "triangle": lattice = Triangle(n_cells, boundary_condition, neighbour_order) return lattice @@ -42,6 +60,17 @@ def transverse_ising( where J is the coupling defined for the Hamiltonian, h is the strength of transverse magnetic field and i,j represent the indices for neighbouring spins. + + Args: + lattice: Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, or ``'Triangle'``. + n_cells: A list containing umber of unit cells in each direction. + coupling: Coupling between spins, it can be a constant or a 2D array of shape number of spins * number of spins. + h: Value of external magnetic field. + boundary_condition: defines boundary conditions, boolean or series of bools with dimensions same as L. + neighbour_order: Range of neighbours a spin interacts with. + + Returns: + pennylane.LinearCombination: Hamiltonian for the transverse-field ising model. """ lattice = generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) @@ -69,6 +98,16 @@ def heisenberg(lattice, n_cells, coupling, boundary_condition=False, neighbour_o \hat{H} = J\sum_{}(\sigma_i^x\sigma_j^x + \sigma_i^y\sigma_j^y + \sigma_i^z\sigma_j^z) where J is the coupling constant defined for the Hamiltonian, and i,j represent the indices for neighbouring spins. + + Args: + lattice: Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, or ``'Triangle'``. + n_cells: A list containing umber of unit cells in each direction. + coupling: Coupling between spins, it can be a 1D array of length 3 or 3D array of shape 3 * number of spins * number of spins. + boundary_condition: defines boundary conditions, boolean or series of bools with dimensions same as L. + neighbour_order: Range of neighbours a spin interacts with. + + Returns: + pennylane.LinearCombination: Hamiltonian for the heisenberg model. """ lattice = generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) @@ -108,6 +147,18 @@ def fermihubbard( \hat{H} = -t\sum_{, \sigma}(c_{i\sigma}^{\dagger}c_{j\sigma}) + U\sum_{i}n_{i \uparrow} n_{i\downarrow} where t is the hopping term representing the kinetic energy of electrons, and U is the on-site Coulomb interaction, representing the repulsion between electrons. + + Args: + lattice: Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, or ``'Triangle'``. + n_cells: A list containing umber of unit cells in each direction. + hopping: hopping between spins, it can be a constant or a 2D array of shape number of spins * number of spins. + interaction: Coulomb interaction between spins, it can be constant or a 2D array of shape number of spins*number of spins. + boundary_condition: defines boundary conditions, boolean or series of bools with dimensions same as L. + neighbour_order: Range of neighbours a spin interacts with. + + Returns: + pennylane.operator: Hamiltonian for the Fermi-Hubbard model. + """ lattice = generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) diff --git a/pennylane/spin/utils.py b/pennylane/spin/utils.py index 12a44d589b7..3c9ecdb5c53 100644 --- a/pennylane/spin/utils.py +++ b/pennylane/spin/utils.py @@ -1,5 +1,25 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. + +# 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 + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains helper functions to create +:class:`~pennylane.spin.lattice` objects. +""" import numpy as np +# pylint: disable=too-many-arguments +# pylint: disable=use-a-generator + def map_vertices(basis_coords, sl, L, basis): """Generates lattice site indices for unit cell + sublattice coordinates.""" @@ -25,7 +45,7 @@ def map_vertices(basis_coords, sl, L, basis): def get_custom_edges( - unit_cell, L, basis, boundary_condition, atol, lattice_points, n_sites, custom_edges + unit_cell, L, basis, boundary_condition, lattice_points, n_sites, custom_edges ): """Generates the edges described in `custom_edges` for all unit cells.""" @@ -54,12 +74,12 @@ def translated_edges(sl1, sl2, distance, operation): # Unit cells of starting points edge_ranges = [] - for i in range(len(L)): + for i, Li in enumerate(L): if boundary_condition[i]: - edge_ranges.append(np.arange(0, L[i])) + edge_ranges.append(np.arange(0, Li)) else: edge_ranges.append( - np.arange(np.maximum(0, -d_cell[i]), L[i] - np.maximum(0, d_cell[i])) + np.arange(np.maximum(0, -d_cell[i]), Li - np.maximum(0, d_cell[i])) ) start_grid = np.meshgrid(*edge_ranges, indexing="ij") From 847273e085a4c900362958a0d6242fca938fa065 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Fri, 16 Aug 2024 11:34:09 -0400 Subject: [PATCH 03/38] Shortened the PR and added more tests --- pennylane/__init__.py | 1 + pennylane/spin/__init__.py | 4 +- pennylane/spin/lattice.py | 60 +++++++------ pennylane/spin/lattice_shapes.py | 1 - pennylane/spin/spin_hamiltonian.py | 121 +++----------------------- pennylane/spin/utils.py | 56 ------------ tests/spin/test_lattice.py | 130 ++++++++++------------------ tests/spin/test_lattice_shapes.py | 106 +++++++++++++++++++++++ tests/spin/test_spin_hamiltonian.py | 61 +++++++++++++ 9 files changed, 257 insertions(+), 283 deletions(-) create mode 100644 tests/spin/test_lattice_shapes.py create mode 100644 tests/spin/test_spin_hamiltonian.py diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 323b243e529..c6efcc6c95a 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -154,6 +154,7 @@ from pennylane.devices.device_constructor import device, refresh_devices import pennylane.spin + # Look for an existing configuration file default_config = Configuration("config.toml") diff --git a/pennylane/spin/__init__.py b/pennylane/spin/__init__.py index 2e8af385e7a..72dfbe7dea6 100644 --- a/pennylane/spin/__init__.py +++ b/pennylane/spin/__init__.py @@ -17,5 +17,5 @@ from .lattice import Lattice from .lattice_shapes import Chain, Square, Rectangle, Honeycomb, Triangle -from .utils import map_vertices, get_custom_edges -from .spin_hamiltonian import generate_lattice, transverse_ising, heisenberg, fermihubbard +from .utils import map_vertices +from .spin_hamiltonian import generate_lattice, transverse_ising diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 7f667feb3d8..bff4e87121a 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -19,7 +19,7 @@ import numpy as np from scipy.spatial import cKDTree -from .utils import map_vertices, get_custom_edges +from .utils import map_vertices # pylint: disable=too-many-arguments, too-many-instance-attributes @@ -33,7 +33,6 @@ class Lattice: basis: Initial positions of spins. boundary_condition: defines boundary conditions, boolean or series of bools with dimensions same as L. neighbour_order: Range of neighbours a spin interacts with. - custom_edges: List of edges in a unit cell along with the operations associated with them. distance_tol: Distance below which spatial points are considered equal for the purpose of identifying nearest neighbours. Returns: @@ -47,7 +46,6 @@ def __init__( basis=None, boundary_condition=False, neighbour_order=1, - custom_edges=None, distance_tol=1e-5, ): @@ -73,24 +71,9 @@ def __init__( self.coords, self.sl_coords, self.lattice_points = self.generate_grid(extra_shells) - if custom_edges is None: - cutoff = neighbour_order * np.linalg.norm(self.unit_cell, axis=1).max() + distance_tol - self.identify_neighbours(cutoff, neighbour_order) - self.generate_true_edges() - else: - if neighbour_order > 1: - raise ValueError( - "custom_edges and neighbour_order cannot be specified at the same time" - ) - self.edges = get_custom_edges( - self.unit_cell, - self.L, - self.basis, - self.boundary_condition, - self.lattice_points, - self.n_sites, - custom_edges, - ) + cutoff = neighbour_order * np.linalg.norm(self.unit_cell, axis=1).max() + distance_tol + self.identify_neighbours(cutoff, neighbour_order) + self.generate_true_edges() def test_input_accuracy(self): r"""Tests the accuracy of the input provided""" @@ -162,20 +145,35 @@ def generate_true_edges(self): for node1, node2 in edge: node1 = map[node1] node2 = map[node2] - if node1 == node2: - raise RuntimeError(f"Lattice contains self-referential edge {(node1, node2)}.") true_edges.add((min(node1, node2), max(node1, node2))) - for edge in true_edges: - colored_edges.append((*edge, k)) + for e in true_edges: + colored_edges.append((*e, k)) self.edges = colored_edges - def add_edge(self, edge_index): - r"""Adds a specific edge based on the site index without translating it""" - - if len(edge_index) == 2: - edge_index = (*edge_index, 0) + def add_edge(self, edge_indices): + r"""Adds a specific edge based on the site index without translating it. + Args: + edge_indices: List of edges to be added. + Returns: + Updates the edges attribute to include provided edges. + """ - self.edges.append(edge_index) + edges_nocolor = [(v1, v2) for (v1, v2, color) in self.edges] + for edge_index in edge_indices: + edge_index = tuple(edge_index) + if len(edge_index) > 3 or len(edge_index) < 2: + raise ValueError("Edge length can only be 2 or 3.") + + if len(edge_index) == 2: + if edge_index in edges_nocolor: + raise ValueError("Edge is already present") + new_edge = (*edge_index, 0) + else: + if edge_index in self.edges: + raise ValueError("Edge is already present") + new_edge = edge_index + + self.edges.append(new_edge) def generate_grid(self, extra_shells): """Generates the coordinates of all lattice sites. diff --git a/pennylane/spin/lattice_shapes.py b/pennylane/spin/lattice_shapes.py index 633db255007..5101c64320c 100644 --- a/pennylane/spin/lattice_shapes.py +++ b/pennylane/spin/lattice_shapes.py @@ -37,7 +37,6 @@ def Square(n_cells, boundary_condition=False, neighbour_order=1): r"""Generates a square lattice""" unit_cell = [[1, 0], [0, 1]] basis = [[0, 0]] - L = n_cells[0:2] lattice_square = Lattice( L=L, diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index 193e3626226..c70e2db85ed 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -15,16 +15,15 @@ This module contains functions to create different templates of spin Hamiltonians. """ -import pennylane as qml -from pennylane import X, Y, Z -from pennylane.fermi import FermiWord +from pennylane import numpy as np +from pennylane import X, Z from .lattice_shapes import Chain, Square, Rectangle, Honeycomb, Triangle # pylint: disable=too-many-arguments -def generate_lattice(lattice, n_cells, boundary_condition, neighbour_order): +def generate_lattice(lattice, n_cells, boundary_condition=False, neighbour_order=1): r"""Generates the lattice object for given attributes.""" lattice_shape = lattice.strip().lower() @@ -42,7 +41,7 @@ def generate_lattice(lattice, n_cells, boundary_condition, neighbour_order): elif lattice_shape == "rectangle": lattice = Rectangle(n_cells, boundary_condition, neighbour_order) elif lattice_shape == "honeycomb": - lattice_shape = Honeycomb(n_cells, boundary_condition, neighbour_order) + lattice = Honeycomb(n_cells, boundary_condition, neighbour_order) elif lattice_shape == "triangle": lattice = Triangle(n_cells, boundary_condition, neighbour_order) @@ -72,13 +71,17 @@ def transverse_ising( Returns: pennylane.LinearCombination: Hamiltonian for the transverse-field ising model. """ - lattice = generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) + coupling = np.asarray(coupling) hamiltonian = 0.0 - if isinstance(coupling, (int, float, complex)): + print(coupling.shape) + if coupling.shape not in [(1,), (lattice.n_sites, lattice.n_sites)]: + raise ValueError(f"Coupling shape should be 1 or {lattice.n_sites}x{lattice.n_sites}") + + if coupling.shape == (1,): for edge in lattice.edges: i, j = edge[0], edge[1] - hamiltonian += -coupling * (Z(i) @ Z(j)) + hamiltonian += -coupling[0] * (Z(i) @ Z(j)) else: for edge in lattice.edges: i, j = edge[0], edge[1] @@ -88,105 +91,3 @@ def transverse_ising( hamiltonian += -h * X(vertex) return hamiltonian - - -def heisenberg(lattice, n_cells, coupling, boundary_condition=False, neighbour_order=1): - r"""Generates the Heisenberg model on a lattice. - The Hamiltonian is represented as: - .. math:: - - \hat{H} = J\sum_{}(\sigma_i^x\sigma_j^x + \sigma_i^y\sigma_j^y + \sigma_i^z\sigma_j^z) - - where J is the coupling constant defined for the Hamiltonian, and i,j represent the indices for neighbouring spins. - - Args: - lattice: Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, or ``'Triangle'``. - n_cells: A list containing umber of unit cells in each direction. - coupling: Coupling between spins, it can be a 1D array of length 3 or 3D array of shape 3 * number of spins * number of spins. - boundary_condition: defines boundary conditions, boolean or series of bools with dimensions same as L. - neighbour_order: Range of neighbours a spin interacts with. - - Returns: - pennylane.LinearCombination: Hamiltonian for the heisenberg model. - """ - - lattice = generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) - - hamiltonian = 0.0 - if isinstance(coupling[0], (int, float, complex)): - for edge in lattice.edges: - i, j = edge[0], edge[1] - hamiltonian += ( - coupling[0] * X(i) @ X(j) + coupling[1] * Y(i) @ Y(j) + coupling[2] * Z(i) @ Z(j) - ) - else: - for edge in lattice.edges: - i, j = edge[0], edge[1] - hamiltonian += ( - coupling[0][i][j] * X(i) @ X(j) - + coupling[1][i][j] * Y(i) @ Y(j) - + coupling[2][i][j] * Z(i) @ Z(j) - ) - - return hamiltonian - - -def fermihubbard( - lattice, - n_cells, - hopping, - interaction, - boundary_condition=False, - neighbour_order=1, - mapping="jordan_wigner", -): - r"""Generates the Hubbard model on a lattice. - The Hamiltonian is represented as: - .. math:: - - \hat{H} = -t\sum_{, \sigma}(c_{i\sigma}^{\dagger}c_{j\sigma}) + U\sum_{i}n_{i \uparrow} n_{i\downarrow} - - where t is the hopping term representing the kinetic energy of electrons, and U is the on-site Coulomb interaction, representing the repulsion between electrons. - - Args: - lattice: Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, or ``'Triangle'``. - n_cells: A list containing umber of unit cells in each direction. - hopping: hopping between spins, it can be a constant or a 2D array of shape number of spins * number of spins. - interaction: Coulomb interaction between spins, it can be constant or a 2D array of shape number of spins*number of spins. - boundary_condition: defines boundary conditions, boolean or series of bools with dimensions same as L. - neighbour_order: Range of neighbours a spin interacts with. - - Returns: - pennylane.operator: Hamiltonian for the Fermi-Hubbard model. - - """ - lattice = generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) - - hamiltonian = 0.0 - if isinstance(hopping, (int, float, complex)): - for edge in lattice.edges: - i, j = edge[0], edge[1] - hopping_term = -hopping * ( - FermiWord({(0, i): "+", (1, j): "-"}) + FermiWord({(0, j): "+", (1, i): "-"}) - ) - int_term = interaction * FermiWord({(0, i): "+", (1, i): "-", (2, j): "+", (3, j): "-"}) - hamiltonian += hopping_term + int_term - else: - for edge in lattice.edges: - i, j = edge[0], edge[1] - hopping_term = -hopping[i][j] * ( - FermiWord({(0, i): "+", (1, j): "-"}) + FermiWord({(0, j): "+", (1, i): "-"}) - ) - int_term = interaction[i][j] * FermiWord( - {(0, i): "+", (1, i): "-", (2, j): "+", (3, j): "-"} - ) - hamiltonian += hopping_term + int_term - - if mapping not in ["jordan_wigner", "parity", "bravyi_kitaev"]: - raise ValueError( - f"The '{mapping}' transformation is not available." - f"Please set mapping to 'jordan_wigner', 'parity', or 'bravyi_kitaev'" - ) - qubit_ham = qml.qchem.qubit_observable(hamiltonian, mapping=mapping) - - return qubit_ham.simplify() diff --git a/pennylane/spin/utils.py b/pennylane/spin/utils.py index 3c9ecdb5c53..4cd760df9c7 100644 --- a/pennylane/spin/utils.py +++ b/pennylane/spin/utils.py @@ -42,59 +42,3 @@ def map_vertices(basis_coords, sl, L, basis): site_indices += sl return site_indices - - -def get_custom_edges( - unit_cell, L, basis, boundary_condition, lattice_points, n_sites, custom_edges -): - """Generates the edges described in `custom_edges` for all unit cells.""" - - if not all([len(edge) in (1, 2) for edge in custom_edges]): - raise TypeError( - """ - custom_edges must be a list of tuples of length 1 or 2. - Every tuple must contain two lattice indices to represent the edge - and can optionally include a list to represent the operation and coefficient for that edge. - """ - ) - - def define_custom_edges(edge): - if edge[0] >= n_sites or edge[1] >= n_sites: - raise ValueError(f"The edge {edge} has vertices greater than n_sites, {n_sites}") - num_sl = len(basis) - sl1 = edge[0] % num_sl - sl2 = edge[1] % num_sl - new_coords = lattice_points[edge[1]] - lattice_points[edge[0]] - return (sl1, sl2, new_coords) - - def translated_edges(sl1, sl2, distance, operation): - # get distance in terms of unit cells, each axis represents the difference between two points in terms of unit_cells - d_cell = (distance + basis[sl1] - basis[sl2]) @ np.linalg.inv(unit_cell) - d_cell = np.asarray(np.rint(d_cell), dtype=int) - - # Unit cells of starting points - edge_ranges = [] - for i, Li in enumerate(L): - if boundary_condition[i]: - edge_ranges.append(np.arange(0, Li)) - else: - edge_ranges.append( - np.arange(np.maximum(0, -d_cell[i]), Li - np.maximum(0, d_cell[i])) - ) - - start_grid = np.meshgrid(*edge_ranges, indexing="ij") - start_grid = np.column_stack([g.ravel() for g in start_grid]) - end_grid = (start_grid + d_cell) % L - - # Convert to site indices - edge_start = map_vertices(start_grid, sl1, L, basis) - edge_end = map_vertices(end_grid, sl2, L, basis) - return [(*edge, operation) for edge in zip(edge_start, edge_end)] - - edges = [] - for i, custom_edge in enumerate(custom_edges): - edge = custom_edge[0] - edge_operation = custom_edge[1] if len(custom_edge) == 2 else i - edge_data = define_custom_edges(edge) - edges += translated_edges(*edge_data, edge_operation) - return edges diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index 1cdbc72b285..37b7a83d77b 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -16,17 +16,18 @@ """ import pytest -import pennylane as qml from pennylane import numpy as np from pennylane.spin import Lattice +# pylint: disable=too-many-arguments, too-many-instance-attributes + def test_boundary_condition_dimension_error(): r"""Test that an error is raised if a wrong dimensions are entered for boundary_condition.""" unit_cell = [[1]] L = [10] with pytest.raises(ValueError, match="Argument 'boundary_condition' must be a bool"): - lattice = Lattice(L=L, unit_cell=unit_cell, boundary_condition=[True, True]) + Lattice(L=L, unit_cell=unit_cell, boundary_condition=[True, True]) def test_boundary_condition_type_error(): @@ -34,7 +35,7 @@ def test_boundary_condition_type_error(): unit_cell = [[1]] L = [10] with pytest.raises(ValueError, match="Argument 'boundary_condition' must be a bool"): - lattice = Lattice(L=L, unit_cell=unit_cell, boundary_condition=[4]) + Lattice(L=L, unit_cell=unit_cell, boundary_condition=[4]) def test_unit_cell_error(): @@ -44,7 +45,7 @@ def test_unit_cell_error(): with pytest.raises( ValueError, match="'unit_cell' must have ndim==2, as array of primitive vectors." ): - lattice = Lattice(L=L, unit_cell=unit_cell) + Lattice(L=L, unit_cell=unit_cell) def test_basis_error(): @@ -55,7 +56,7 @@ def test_basis_error(): with pytest.raises( ValueError, match="'basis' must have ndim==2, as array of initial coordinates." ): - lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis) + Lattice(L=L, unit_cell=unit_cell, basis=basis) def test_unit_cell_shape_error(): @@ -63,7 +64,7 @@ def test_unit_cell_shape_error(): unit_cell = [[0, 1, 2], [0, 1, 1]] L = [2, 2] with pytest.raises(ValueError, match="The number of primitive vectors must match their length"): - lattice = Lattice(L=L, unit_cell=unit_cell) + Lattice(L=L, unit_cell=unit_cell) def test_L_error(): @@ -72,7 +73,7 @@ def test_L_error(): unit_cell = [[0, 1], [1, 0]] L = [2, -2] with pytest.raises(TypeError, match="Argument `L` must be a list of positive integers"): - lattice = Lattice(L=L, unit_cell=unit_cell) + Lattice(L=L, unit_cell=unit_cell) def test_L_type_error(): @@ -81,39 +82,7 @@ def test_L_type_error(): unit_cell = [[0, 1], [1, 0]] L = [2, 2.4] with pytest.raises(TypeError, match="Argument `L` must be a list of positive integers"): - lattice = Lattice(L=L, unit_cell=unit_cell) - - -def test_neighbour_order_error(): - r"""Test that an error is raised if neighbour order is greater than 1 when custom_edges are provided.""" - - unit_cell = [[0, 1], [1, 0]] - L = [3, 3] - custom_edges = [[(0, 1)], [(0, 5)], [(0, 4)]] - with pytest.raises( - ValueError, match="custom_edges and neighbour_order cannot be specified at the same time" - ): - lattice = Lattice(L=L, unit_cell=unit_cell, neighbour_order=2, custom_edges=custom_edges) - - -def test_custom_edge_type_error(): - r"""Test that an error is raised if custom_edges are not provided as a list of length 1 or 2.""" - - unit_cell = [[0, 1], [1, 0]] - L = [3, 3] - custom_edges = [[(0, 1), 1, 3], [(0, 5)], [(0, 4)]] - with pytest.raises(TypeError, match="custom_edges must be a list of tuples of length 1 or 2."): - lattice = Lattice(L=L, unit_cell=unit_cell, custom_edges=custom_edges) - - -def test_custom_edge_value_error(): - r"""Test that an error is raised if the custom_edges contains an edge with site_index greater than number of sites""" - - unit_cell = [[0, 1], [1, 0]] - L = [3, 3] - custom_edges = [[(0, 1)], [(0, 5)], [(0, 12)]] - with pytest.raises(ValueError, match="The edge \(0, 12\) has vertices greater than n_sites, 9"): - lattice = Lattice(L=L, unit_cell=unit_cell, custom_edges=custom_edges) + Lattice(L=L, unit_cell=unit_cell) @pytest.mark.parametrize( @@ -129,41 +98,8 @@ def test_basis(unit_cell, basis, L): r"""Test that the lattice points start from the coordinates provided in the basis""" lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis) - for i in range(len(basis)): - assert np.allclose(basis[i], lattice.lattice_points[i]) - - -@pytest.mark.parametrize( - # expected_edges here were obtained manually - ("unit_cell", "basis", "L", "custom_edges", "expected_edges"), - [ - ( - [[0, 1], [1, 0]], - [[0, 0]], - [3, 3], - [[(0, 1)], [(0, 5)], [(0, 4)]], - [(0, 1, 0), (0, 5, 1), (0, 4, 2), (1, 2, 0), (3, 4, 0), (0, 4, 2), (1, 5, 2)], - ), - ( - [[0, 1], [1, 0]], - [[0, 0]], - [3, 4], - [[(0, 1)], [(1, 4)], [(1, 5)]], - [(0, 1, 0), (1, 2, 0), (2, 3, 0), (1, 4, 1), (2, 5, 1), (0, 4, 2), (2, 6, 2)], - ), - ( - [[1, 0], [0.5, np.sqrt(3) / 2]], - [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], - [2, 2], - [[(0, 1)], [(1, 2)], [(1, 5)]], - [(2, 3, 0), (4, 5, 0), (1, 2, 1), (5, 6, 1), (1, 5, 2), (3, 7, 2)], - ), - ], -) -def test_custom_edges(unit_cell, basis, L, custom_edges, expected_edges): - r"""Test that the edges are added as per custom_edges provided""" - lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis, custom_edges=custom_edges) - assert np.all(np.isin(expected_edges, lattice.edges)) + for i, b in enumerate(basis): + assert np.allclose(b, lattice.lattice_points[i]) @pytest.mark.parametrize( @@ -363,16 +299,44 @@ def test_neighbour_order(unit_cell, basis, L, neighbour_order, expected_edges): ([[0, 1], [1, 0]], [[1.5, 1.5]], [3, 3], True, 2, [True, True]), ([[0, 1], [1, 0]], [[-1, -1]], [3, 3], False, 2, [False, False]), ([[0, 1], [1, 0]], [[10, 10]], [3, 3], [True, False], 2, [True, False]), - ([[1, 0], [0.5, np.sqrt(3) / 2]], [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], [2, 2], [True, True], 2, [True, True]), - (np.eye(3), [[0,0,0]], [3, 3, 4], True, 3, [True, True, True]), + ( + [[1, 0], [0.5, np.sqrt(3) / 2]], + [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], + [2, 2], + [True, True], + 2, + [True, True], + ), + (np.eye(3), [[0, 0, 0]], [3, 3, 4], True, 3, [True, True, True]), ], ) - def test_attributes(unit_cell, basis, L, boundary_condition, n_dim, expected_bc): r"""Test that the methods and attributes return correct values""" - lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis,boundary_condition=boundary_condition) + lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis, boundary_condition=boundary_condition) + + assert np.all(lattice.unit_cell == unit_cell) + assert np.all(lattice.basis == basis) + assert lattice.n_dim == n_dim + assert np.all(lattice.boundary_condition == expected_bc) + + +def test_add_edge_error(): + r"""Test that an error is raised if the added edge is already present for a lattice""" + edge_indices = [[4, 5]] + unit_cell = [[0, 1], [1, 0]] + L = [3, 3] + lattice = Lattice(L=L, unit_cell=unit_cell) + + with pytest.raises(ValueError, match="Edge is already present"): + lattice.add_edge(edge_indices) + + +def test_add_edge(): + r"""Test that edges are added per their index to a lattice""" + edge_indices = [[1, 3], [4, 6]] + unit_cell = [[0, 1], [1, 0]] + L = [3, 3] + lattice = Lattice(L=L, unit_cell=unit_cell) - assert(np.all(lattice.unit_cell == unit_cell)) - assert(np.all(lattice.basis == basis)) - assert(lattice.n_dim == n_dim) - assert(np.all(lattice.boundary_condition == expected_bc)) + lattice.add_edge(edge_indices) + assert np.all(np.isin(edge_indices, lattice.edges)) diff --git a/tests/spin/test_lattice_shapes.py b/tests/spin/test_lattice_shapes.py new file mode 100644 index 00000000000..03c79a18005 --- /dev/null +++ b/tests/spin/test_lattice_shapes.py @@ -0,0 +1,106 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. + +# 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 + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests for functions needed for computing the lattices of certain shapes. +""" + +import pytest +from pennylane.spin import generate_lattice + + +@pytest.mark.parametrize( + # expected_edges here were obtained with netket. + ("shape", "L", "expected_edges"), + [ + ( + "chAin ", + [10, 0, 0], + [ + (0, 1, 0), + (1, 2, 0), + (3, 4, 0), + (2, 3, 0), + (6, 7, 0), + (4, 5, 0), + (8, 9, 0), + (5, 6, 0), + (7, 8, 0), + ], + ), + ( + "Square", + [3, 3], + [ + (0, 1, 0), + (1, 2, 0), + (3, 4, 0), + (5, 8, 0), + (0, 3, 0), + (1, 4, 0), + (6, 7, 0), + (4, 5, 0), + (3, 6, 0), + (2, 5, 0), + (4, 7, 0), + (7, 8, 0), + ], + ), + ( + " Rectangle ", + [3, 4], + [ + (0, 1, 0), + (9, 10, 0), + (1, 2, 0), + (0, 4, 0), + (10, 11, 0), + (1, 5, 0), + (3, 7, 0), + (2, 3, 0), + (6, 7, 0), + (4, 5, 0), + (8, 9, 0), + (2, 6, 0), + (5, 6, 0), + (4, 8, 0), + (6, 10, 0), + (5, 9, 0), + (7, 11, 0), + ], + ), + ( + "honeycomb", + [2, 2], + [ + (0, 1, 0), + (1, 2, 0), + (1, 4, 0), + (2, 3, 0), + (6, 7, 0), + (4, 5, 0), + (5, 6, 0), + (3, 6, 0), + ], + ), + ( + "TRIANGLE", + [2, 2], + [(0, 1, 0), (1, 2, 0), (2, 3, 0), (0, 2, 0), (1, 3, 0)], + ), + ], +) +def test_edges_for_shapes(shape, L, expected_edges): + r"""Test that correct edges are obtained for given lattice shapes""" + lattice = generate_lattice(lattice=shape, n_cells=L) + assert lattice.edges == expected_edges diff --git a/tests/spin/test_spin_hamiltonian.py b/tests/spin/test_spin_hamiltonian.py new file mode 100644 index 00000000000..43751f23bda --- /dev/null +++ b/tests/spin/test_spin_hamiltonian.py @@ -0,0 +1,61 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. + +# 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 + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests for functions needed for computing the spin Hamiltonians. +""" + +import pytest + +import pennylane as qml +from pennylane.spin import transverse_ising + + +def test_coupling_error(): + r"""Test that an error is raised when the provided coupling shape is wrong""" + n_cells = [4, 4] + lattice = "Square" + with pytest.raises(ValueError, match="Coupling shape should be 1 or 16x16"): + transverse_ising(lattice=lattice, n_cells=n_cells, coupling=1.0) + + +def test_shape_error(): + r"""Test that an error is raised if wrong shape is provided""" + n_cells = [5, 5, 5] + lattice = "Octagon" + with pytest.raises(ValueError, match="Lattice shape, 'Octagon' is not supported."): + transverse_ising(lattice=lattice, n_cells=n_cells, coupling=1.0) + + +@pytest.mark.parametrize( + ("shape_ds", "shape_lattice", "layout", "n_cells"), + [ + ("chain", "chain", "1x4", [4, 0, 0]), + ("chain", "chain", "1x8", [8, 0, 0]), + ("rectangular", "rectangle", "2x4", [4, 2, 0]), + ("rectangular", "rectangle", "2x8", [8, 2, 0]), + ], +) +def test_ising_hamiltonian(shape_ds, shape_lattice, layout, n_cells): + r"""Test that the correct Hamiltonian is generated compared to the datasets""" + spin_dataset = qml.data.load("qspin", sysname="Ising", lattice=shape_ds, layout=layout) + dataset_ham = spin_dataset[0].hamiltonians[0] + + J = [-spin_dataset[0].parameters["J"]] + h = -spin_dataset[0].parameters["h"][0] + + ising_ham = transverse_ising( + lattice=shape_lattice, n_cells=n_cells, coupling=J, h=h, neighbour_order=1 + ) + + assert qml.equal(ising_ham, dataset_ham) From 1df7f0c256ae0675723e86af4ae06034b2a5ade3 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Mon, 19 Aug 2024 10:58:17 -0400 Subject: [PATCH 04/38] Cleaned up some --- pennylane/spin/__init__.py | 3 +- pennylane/spin/lattice.py | 135 ++++++++++++---------------- pennylane/spin/lattice_shapes.py | 1 + pennylane/spin/spin_hamiltonian.py | 16 ++-- pennylane/spin/utils.py | 44 --------- tests/spin/test_lattice.py | 4 +- tests/spin/test_lattice_shapes.py | 3 +- tests/spin/test_spin_hamiltonian.py | 4 +- 8 files changed, 75 insertions(+), 135 deletions(-) delete mode 100644 pennylane/spin/utils.py diff --git a/pennylane/spin/__init__.py b/pennylane/spin/__init__.py index 72dfbe7dea6..720d9f1c3d5 100644 --- a/pennylane/spin/__init__.py +++ b/pennylane/spin/__init__.py @@ -16,6 +16,5 @@ """ from .lattice import Lattice -from .lattice_shapes import Chain, Square, Rectangle, Honeycomb, Triangle -from .utils import map_vertices +from .lattice_shapes import Chain, Honeycomb, Rectangle, Square, Triangle from .spin_hamiltonian import generate_lattice, transverse_ising diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index bff4e87121a..6c6e610a0b2 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -16,12 +16,13 @@ :class:`~pennylane.spin.lattice` object. This object stores all the necessary information about a lattice. """ +import itertools import numpy as np from scipy.spatial import cKDTree -from .utils import map_vertices # pylint: disable=too-many-arguments, too-many-instance-attributes +# pylint: disable=use-a-generator, too-few-public-methods class Lattice: @@ -54,6 +55,7 @@ def __init__( self.unit_cell = np.asarray(unit_cell) if basis is None: basis = np.zeros(self.unit_cell.shape[0])[None, :] + self.basis = np.asarray(basis) self.n_sl = len(self.basis) self.n_sites = np.prod(L) * self.n_sl @@ -62,20 +64,15 @@ def __init__( boundary_condition = [boundary_condition for _ in range(self.n_dim)] self.boundary_condition = boundary_condition - self.test_input_accuracy() - - if True in self.boundary_condition: - extra_shells = np.where(self.boundary_condition, neighbour_order, 0) - else: - extra_shells = None + self._test_input_accuracy() - self.coords, self.sl_coords, self.lattice_points = self.generate_grid(extra_shells) + self.lattice_points, lattice_map = self._generate_grid(neighbour_order) cutoff = neighbour_order * np.linalg.norm(self.unit_cell, axis=1).max() + distance_tol - self.identify_neighbours(cutoff, neighbour_order) - self.generate_true_edges() + edges = self._identify_neighbours(cutoff) + self._generate_true_edges(edges, lattice_map, neighbour_order) - def test_input_accuracy(self): + def _test_input_accuracy(self): r"""Tests the accuracy of the input provided""" for l in self.L: @@ -101,16 +98,14 @@ def test_input_accuracy(self): "Argument 'boundary_condition' must be a bool or a list of bools of same dimensions as the unit_cell" ) - def identify_neighbours(self, cutoff, neighbour_order): + def _identify_neighbours(self, cutoff): r"""Identifies the connections between lattice points and returns the unique connections based on the neighbour_order""" tree = cKDTree(self.lattice_points) indices = tree.query_ball_tree(tree, cutoff) unique_pairs = set() - row = [] - col = [] - distance = [] + edges = {} for i, neighbours in enumerate(indices): for neighbour in neighbours: if neighbour != i: @@ -120,35 +115,60 @@ def identify_neighbours(self, cutoff, neighbour_order): dist = np.linalg.norm( self.lattice_points[i] - self.lattice_points[neighbour] ) - row.append(i) - col.append(neighbour) - distance.append(dist) + # Scale the distance + bin_density = 21621600 # multiple of expected denominators + scaled_dist = np.rint(dist * bin_density) - row = np.array(row) - col = np.array(col) + if scaled_dist not in edges: + edges[scaled_dist] = [] + edges[scaled_dist].append((i, neighbour)) + return edges - # Sort distance into bins for comparison - bin_density = 21621600 # multiple of expected denominators - distance = np.asarray(np.rint(np.asarray(distance) * bin_density), dtype=int) + def _generate_true_edges(self, edges, map, neighbour_order): + r"""Modifies the edges to remove hidden nodes and create connections based on boundary_conditions""" - _, ii = np.unique(distance, return_inverse=True) + self.edges = [] + for i, (_, edge) in enumerate(sorted(edges.items())): + if i >= neighbour_order: + break + for e1, e2 in edge: + true_edge = (min(map[e1], map[e2]), max(map[e1], map[e2]), i) + if true_edge not in self.edges: + self.edges.append(true_edge) - self.edges = [sorted(list(zip(row[ii == k], col[ii == k]))) for k in range(neighbour_order)] + def _generate_grid(self, neighbour_order): + """Generates the coordinates of all lattice sites and their indices. - def generate_true_edges(self): - r"""Modifies the edges to remove hidden nodes and create connections based on boundary_conditions""" + Args: + neighbour_order: The number of nearest neighbour interactions. + Returns: + lattice_points: The coordinates of all lattice sites. + lattice_map: A list to represent the node number for each lattice_point + """ - map = map_vertices(self.coords, self.sl_coords, self.L, self.basis) - colored_edges = [] - for k, edge in enumerate(self.edges): - true_edges = set() - for node1, node2 in edge: - node1 = map[node1] - node2 = map[node2] - true_edges.add((min(node1, node2), max(node1, node2))) - for e in true_edges: - colored_edges.append((*e, k)) - self.edges = colored_edges + n_sl = len(self.basis) + if self.boundary_condition: + wrap_grid = np.where(self.boundary_condition, neighbour_order, 0) + else: + wrap_grid = np.zeros(self.L.size, dtype=int) + + ranges_dim = [range(-wrap_grid[i], Lx + wrap_grid[i]) for i, Lx in enumerate(self.L)] + ranges_dim.append(range(n_sl)) + lattice_points = [] + lattice_map = [] + + for Lx in itertools.product(*ranges_dim): + point = np.dot(Lx[:-1], self.unit_cell) + self.basis[Lx[-1]] + node_index = 0 + for i in range(self.n_dim): + node_index += ( + (Lx[i] % self.L[i]) * np.prod(self.L[self.n_dim - 1 - i : 0 : -1]) * n_sl + ) + node_index += Lx[-1] + lattice_points.append(point) + lattice_map.append(node_index) + + return np.array(lattice_points), np.array(lattice_map) def add_edge(self, edge_indices): r"""Adds a specific edge based on the site index without translating it. @@ -174,42 +194,3 @@ def add_edge(self, edge_indices): new_edge = edge_index self.edges.append(new_edge) - - def generate_grid(self, extra_shells): - """Generates the coordinates of all lattice sites. - - Args: - extra_shells (np.ndarray): Optional. The number of unit cells added along each lattice direction. - This is used for near-neighbour searching in periodic boundary conditions (PBC). - It must be a vector of the same length as L. - - Returns: - basis_coords: The coordinates of the basis sites in each unit cell. - sl_coords: The coordinates of sublattice sites in each lattice site. - lattice_points: The coordinates of all lattice sites. - """ - - # Initialize extra_shells if not provided - if extra_shells is None: - extra_shells = np.zeros(self.L.size, dtype=int) - - shell_min = -extra_shells - shell_max = self.L + extra_shells - - range_dim = [] - for i in range(self.n_dim): - range_dim.append(np.arange(shell_min[i], shell_max[i])) - - range_dim.append(np.arange(0, self.n_sl)) - - coords = np.meshgrid(*range_dim, indexing="ij") - - sl_coords = coords[-1].ravel() - basis_coords = np.column_stack([c.ravel() for c in coords[:-1]]) - - lattice_points = (np.dot(basis_coords, self.unit_cell)).astype(float) - - for i in range(0, len(lattice_points), self.n_sl): - lattice_points[i : i + self.n_sl] = lattice_points[i : i + self.n_sl] + self.basis - - return basis_coords, sl_coords, lattice_points diff --git a/pennylane/spin/lattice_shapes.py b/pennylane/spin/lattice_shapes.py index 5101c64320c..0ad4d27c5b8 100644 --- a/pennylane/spin/lattice_shapes.py +++ b/pennylane/spin/lattice_shapes.py @@ -17,6 +17,7 @@ """ from pennylane import numpy as np + from .lattice import Lattice diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index c70e2db85ed..2d1b934c7fa 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -15,10 +15,10 @@ This module contains functions to create different templates of spin Hamiltonians. """ -from pennylane import numpy as np from pennylane import X, Z +from pennylane import numpy as np -from .lattice_shapes import Chain, Square, Rectangle, Honeycomb, Triangle +from .lattice_shapes import Chain, Honeycomb, Rectangle, Square, Triangle # pylint: disable=too-many-arguments @@ -75,13 +75,15 @@ def transverse_ising( coupling = np.asarray(coupling) hamiltonian = 0.0 print(coupling.shape) - if coupling.shape not in [(1,), (lattice.n_sites, lattice.n_sites)]: - raise ValueError(f"Coupling shape should be 1 or {lattice.n_sites}x{lattice.n_sites}") + if coupling.shape not in [(neighbour_order,), (lattice.n_sites, lattice.n_sites)]: + raise ValueError( + f"Coupling shape should be equal to {neighbour_order} or {lattice.n_sites}x{lattice.n_sites}" + ) - if coupling.shape == (1,): + if coupling.shape == (neighbour_order,): for edge in lattice.edges: - i, j = edge[0], edge[1] - hamiltonian += -coupling[0] * (Z(i) @ Z(j)) + i, j, order = edge[0], edge[1], edge[2] + hamiltonian += -coupling[order] * (Z(i) @ Z(j)) else: for edge in lattice.edges: i, j = edge[0], edge[1] diff --git a/pennylane/spin/utils.py b/pennylane/spin/utils.py deleted file mode 100644 index 4cd760df9c7..00000000000 --- a/pennylane/spin/utils.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2018-2024 Xanadu Quantum Technologies Inc. - -# 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 - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains helper functions to create -:class:`~pennylane.spin.lattice` objects. -""" -import numpy as np - -# pylint: disable=too-many-arguments -# pylint: disable=use-a-generator - - -def map_vertices(basis_coords, sl, L, basis): - """Generates lattice site indices for unit cell + sublattice coordinates.""" - - basis_coords = basis_coords % L - - site_indices = np.zeros(basis_coords.shape[0], dtype=int) - - num_sl = len(basis) - num_dim = len(L) - - nsites_axis = np.zeros(num_dim, dtype=int) - nsites_axis[-1] = num_sl - - for j in range(num_dim - 1, 0, -1): - nsites_axis[j - 1] = nsites_axis[j] * L[num_dim - j] - - for index in range(basis_coords.shape[0]): - site_indices[index] = np.dot(basis_coords[index], nsites_axis) - site_indices += sl - - return site_indices diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index 37b7a83d77b..ee975010674 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -202,7 +202,7 @@ def test_lattice_points(unit_cell, basis, L, expected_number): def test_boundary_condition(unit_cell, basis, L, boundary_condition, expected_edges): r"""Test that the correct edges are obtained for given boundary conditions""" lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis, boundary_condition=boundary_condition) - assert lattice.edges == expected_edges + assert sorted(lattice.edges) == sorted(expected_edges) @pytest.mark.parametrize( @@ -290,7 +290,7 @@ def test_boundary_condition(unit_cell, basis, L, boundary_condition, expected_ed def test_neighbour_order(unit_cell, basis, L, neighbour_order, expected_edges): r"""Test that the correct edges are obtained for given neighbour order""" lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis, neighbour_order=neighbour_order) - assert lattice.edges == expected_edges + assert sorted(lattice.edges) == sorted(expected_edges) @pytest.mark.parametrize( diff --git a/tests/spin/test_lattice_shapes.py b/tests/spin/test_lattice_shapes.py index 03c79a18005..4d63c454ba3 100644 --- a/tests/spin/test_lattice_shapes.py +++ b/tests/spin/test_lattice_shapes.py @@ -16,6 +16,7 @@ """ import pytest + from pennylane.spin import generate_lattice @@ -103,4 +104,4 @@ def test_edges_for_shapes(shape, L, expected_edges): r"""Test that correct edges are obtained for given lattice shapes""" lattice = generate_lattice(lattice=shape, n_cells=L) - assert lattice.edges == expected_edges + assert sorted(lattice.edges) == sorted(expected_edges) diff --git a/tests/spin/test_spin_hamiltonian.py b/tests/spin/test_spin_hamiltonian.py index 43751f23bda..3299aae1faf 100644 --- a/tests/spin/test_spin_hamiltonian.py +++ b/tests/spin/test_spin_hamiltonian.py @@ -25,8 +25,8 @@ def test_coupling_error(): r"""Test that an error is raised when the provided coupling shape is wrong""" n_cells = [4, 4] lattice = "Square" - with pytest.raises(ValueError, match="Coupling shape should be 1 or 16x16"): - transverse_ising(lattice=lattice, n_cells=n_cells, coupling=1.0) + with pytest.raises(ValueError, match="Coupling shape should be equal to 1 or 16x16"): + transverse_ising(lattice=lattice, n_cells=n_cells, coupling=1.0, neighbour_order=1) def test_shape_error(): From 42b994a7b1c7af2ab1089caba3b0899c7373f861 Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Mon, 19 Aug 2024 12:28:38 -0400 Subject: [PATCH 05/38] Update pennylane/spin/__init__.py Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com> --- pennylane/spin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/__init__.py b/pennylane/spin/__init__.py index 720d9f1c3d5..a165b3130bb 100644 --- a/pennylane/spin/__init__.py +++ b/pennylane/spin/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This submodule provides the functionality to obtain spin Hamiltonians. +This submodule provides the functionality to create spin Hamiltonians. """ from .lattice import Lattice From dbdb67734cce1cba861e3744483e28ae764438a1 Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Mon, 19 Aug 2024 12:29:11 -0400 Subject: [PATCH 06/38] Update pennylane/spin/lattice.py Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com> --- pennylane/spin/lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 6c6e610a0b2..57007367cb9 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -29,7 +29,7 @@ class Lattice: r"""Constructs a Lattice object. Args: - L: Number of unit cells in a direction, it is a list depending on the dimensions of the lattice. + L: Number of unit cells in each direction. unit_cell: Primitive vectors for the lattice. basis: Initial positions of spins. boundary_condition: defines boundary conditions, boolean or series of bools with dimensions same as L. From b139df7a686d9154fd8d2f3e3e47031bba1610fd Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Tue, 20 Aug 2024 07:07:18 -0400 Subject: [PATCH 07/38] Update pennylane/spin/__init__.py Co-authored-by: Utkarsh --- pennylane/spin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/__init__.py b/pennylane/spin/__init__.py index a165b3130bb..a59c6baf368 100644 --- a/pennylane/spin/__init__.py +++ b/pennylane/spin/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This submodule provides the functionality to create spin Hamiltonians. +This module provides the functionality to create Lattices and spin Hamiltonians. """ from .lattice import Lattice From d42e6ed3a8f64b592896c609cb546716ba9be8fe Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Tue, 20 Aug 2024 07:07:41 -0400 Subject: [PATCH 08/38] Update pennylane/spin/lattice.py Co-authored-by: Utkarsh --- pennylane/spin/lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 57007367cb9..18157b2b82c 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains functions and classes to create a +This file contains functions and classes to create a :class:`~pennylane.spin.lattice` object. This object stores all the necessary information about a lattice. """ From d6df72774f6f22174ff63e19606befc9053f049c Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Tue, 20 Aug 2024 07:13:31 -0400 Subject: [PATCH 09/38] Update pennylane/spin/lattice.py Co-authored-by: Utkarsh --- pennylane/spin/lattice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 18157b2b82c..0bfff0c71ca 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -172,6 +172,7 @@ def _generate_grid(self, neighbour_order): def add_edge(self, edge_indices): r"""Adds a specific edge based on the site index without translating it. + Args: edge_indices: List of edges to be added. Returns: From dcee93b839c621a55096595defb6e125e5956a51 Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Tue, 20 Aug 2024 07:13:57 -0400 Subject: [PATCH 10/38] Update pennylane/spin/spin_hamiltonian.py Co-authored-by: Utkarsh --- pennylane/spin/spin_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index 2d1b934c7fa..d48b44027e0 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains functions to create different templates of spin Hamiltonians. +This file contains functions to create different templates of spin Hamiltonians. """ from pennylane import X, Z From 981c0fb418e942cf7cbab106593ccc8641ddf266 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Tue, 20 Aug 2024 13:10:49 -0400 Subject: [PATCH 11/38] [skip ci] Addressed reviews --- doc/releases/changelog-dev.md | 5 + pennylane/spin/__init__.py | 3 +- pennylane/spin/lattice.py | 266 ++++++++++++++++++------ pennylane/spin/lattice_shapes.py | 101 --------- pennylane/spin/spin_hamiltonian.py | 67 +++--- tests/spin/test_lattice.py | 307 +++++++++++++++++++--------- tests/spin/test_lattice_shapes.py | 107 ---------- tests/spin/test_spin_hamiltonian.py | 119 +++++++++-- 8 files changed, 560 insertions(+), 415 deletions(-) delete mode 100644 pennylane/spin/lattice_shapes.py delete mode 100644 tests/spin/test_lattice_shapes.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index efb207c4b1e..a4a11dcef0b 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -41,6 +41,10 @@ users to have more control over the error mitigation protocol without needing to add further dependencies. [(#5972)](https://github.com/PennyLaneAI/pennylane/pull/5972) +* New function to generate transverse-field Ising Hamiltonian and the required helper functions to generate the lattice + have been added. + [(#6106)](https://github.com/PennyLaneAI/pennylane/pull/6106) +

Improvements 🛠

* `QNGOptimizer` now supports cost functions with multiple arguments, updating each argument independently. @@ -342,6 +346,7 @@ Ahmed Darwish, Astral Cai, Yushao Chen, Ahmed Darwish, +Diksha Dhawan, Maja Franz, Lillian M. A. Frederiksen, Pietropaolo Frisoni, diff --git a/pennylane/spin/__init__.py b/pennylane/spin/__init__.py index a59c6baf368..d24bda7b2df 100644 --- a/pennylane/spin/__init__.py +++ b/pennylane/spin/__init__.py @@ -16,5 +16,4 @@ """ from .lattice import Lattice -from .lattice_shapes import Chain, Honeycomb, Rectangle, Square, Triangle -from .spin_hamiltonian import generate_lattice, transverse_ising +from .spin_hamiltonian import transverse_ising diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 0bfff0c71ca..6da346b4760 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -19,7 +19,9 @@ import itertools import numpy as np -from scipy.spatial import cKDTree +from scipy.spatial import KDTree + +from pennylane import math # pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=use-a-generator, too-few-public-methods @@ -29,81 +31,91 @@ class Lattice: r"""Constructs a Lattice object. Args: - L: Number of unit cells in each direction. - unit_cell: Primitive vectors for the lattice. - basis: Initial positions of spins. - boundary_condition: defines boundary conditions, boolean or series of bools with dimensions same as L. - neighbour_order: Range of neighbours a spin interacts with. - distance_tol: Distance below which spatial points are considered equal for the purpose of identifying nearest neighbours. + n_cells (list[int]): Number of cells in each direction of the grid. + vectors (list[list[float]]): Primitive vectors for the lattice. + positions (list[list[float]]): Initial positions of spins. Default value is [[0.0]*number of dimensions]. + boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, each element represents the axis for lattice. It defaults to False. + neighbour_order (int): Range of neighbours a spin interacts with. Default is 1. + distance_tol (float): Distance below which spatial points are considered equal for the purpose of identifying nearest neighbours, default value is 1e-5. Returns: Lattice object + + **Example** + >>> n_cells = [2,2] + >>> vectors = [[0, 1], [1, 0]] + >>> boundary_condition = [True, False] + >>> lattice = Lattice(n_cells, vectors, + >>> boundary_condition=boundary_condition) + >>> print(lattice.edges) + [(3, 4, 0), (0, 3, 0), (4, 5, 0), (1, 4, 0), + (2, 5, 0), (0, 1, 0), (1, 2, 0)] """ def __init__( self, - L, - unit_cell, - basis=None, + n_cells, + vectors, + positions=None, boundary_condition=False, neighbour_order=1, distance_tol=1e-5, ): - self.L = np.asarray(L) - self.n_dim = len(L) - self.unit_cell = np.asarray(unit_cell) - if basis is None: - basis = np.zeros(self.unit_cell.shape[0])[None, :] - - self.basis = np.asarray(basis) - self.n_sl = len(self.basis) - self.n_sites = np.prod(L) * self.n_sl - - if isinstance(boundary_condition, bool): - boundary_condition = [boundary_condition for _ in range(self.n_dim)] - - self.boundary_condition = boundary_condition - self._test_input_accuracy() - - self.lattice_points, lattice_map = self._generate_grid(neighbour_order) + for l in math.asarray(n_cells): + if (not isinstance(l, np.int64)) or l <= 0: + raise TypeError("Argument `n_cells` must be a list of positive integers") - cutoff = neighbour_order * np.linalg.norm(self.unit_cell, axis=1).max() + distance_tol - edges = self._identify_neighbours(cutoff) - self._generate_true_edges(edges, lattice_map, neighbour_order) + vectors = math.asarray(vectors) - def _test_input_accuracy(self): - r"""Tests the accuracy of the input provided""" + if vectors.ndim != 2: + raise ValueError("'vectors' must have ndim==2, as array of primitive vectors.") - for l in self.L: - if (not isinstance(l, np.int64)) or l <= 0: - raise TypeError("Argument `L` must be a list of positive integers") + if vectors.shape[0] != vectors.shape[1]: + raise ValueError("The number of primitive vectors must match their length") - if self.unit_cell.ndim != 2: - raise ValueError("'unit_cell' must have ndim==2, as array of primitive vectors.") + if positions is None: + positions = math.zeros(vectors.shape[0])[None, :] + positions = math.asarray(positions) - if self.basis.ndim != 2: - raise ValueError("'basis' must have ndim==2, as array of initial coordinates.") + if positions.ndim != 2: + raise ValueError("'positions' must have ndim==2, as array of initial coordinates.") - if self.unit_cell.shape[0] != self.unit_cell.shape[1]: - raise ValueError("The number of primitive vectors must match their length") + if isinstance(boundary_condition, bool): + boundary_condition = [boundary_condition for _ in range(len(n_cells))] - if not all(isinstance(b, bool) for b in self.boundary_condition): + if not all(isinstance(b, bool) for b in boundary_condition): raise ValueError( - "Argument 'boundary_condition' must be a bool or a list of bools of same dimensions as the unit_cell" + "Argument 'boundary_condition' must be a bool or a list of bools of same dimensions as the vectors" ) - if len(self.boundary_condition) != self.n_dim: + if len(boundary_condition) != len(n_cells): raise ValueError( - "Argument 'boundary_condition' must be a bool or a list of bools of same dimensions as the unit_cell" + "Argument 'boundary_condition' must be a bool or a list of bools of same dimensions as the vectors" ) + self.n_cells = n_cells + self.n_dim = len(n_cells) + self.vectors = math.asarray(vectors) + self.positions = math.asarray(positions) + self.boundary_condition = boundary_condition + + n_sl = len(self.positions) + self.n_sites = math.prod(n_cells) * n_sl + self.lattice_points, lattice_map = self._generate_grid(neighbour_order) + + cutoff = neighbour_order * math.linalg.norm(self.vectors, axis=1).max() + distance_tol + edges = self._identify_neighbours(cutoff) + self._generate_true_edges(edges, lattice_map, neighbour_order) + def _identify_neighbours(self, cutoff): r"""Identifies the connections between lattice points and returns the unique connections based on the neighbour_order""" - tree = cKDTree(self.lattice_points) + tree = KDTree(self.lattice_points) indices = tree.query_ball_tree(tree, cutoff) + # Number to Scale the distance + bin_density = 21621600 # multiple of expected denominators unique_pairs = set() edges = {} for i, neighbours in enumerate(indices): @@ -112,23 +124,23 @@ def _identify_neighbours(self, cutoff): pair = (min(i, neighbour), max(i, neighbour)) if pair not in unique_pairs: unique_pairs.add(pair) - dist = np.linalg.norm( + dist = math.linalg.norm( self.lattice_points[i] - self.lattice_points[neighbour] ) - # Scale the distance - bin_density = 21621600 # multiple of expected denominators - scaled_dist = np.rint(dist * bin_density) + scaled_dist = math.rint(dist * bin_density) if scaled_dist not in edges: edges[scaled_dist] = [] edges[scaled_dist].append((i, neighbour)) + + edges = [value for key, value in sorted(edges.items())] return edges def _generate_true_edges(self, edges, map, neighbour_order): r"""Modifies the edges to remove hidden nodes and create connections based on boundary_conditions""" self.edges = [] - for i, (_, edge) in enumerate(sorted(edges.items())): + for i, edge in enumerate(edges): if i >= neighbour_order: break for e1, e2 in edge: @@ -146,33 +158,35 @@ def _generate_grid(self, neighbour_order): lattice_map: A list to represent the node number for each lattice_point """ - n_sl = len(self.basis) + n_sl = len(self.positions) if self.boundary_condition: - wrap_grid = np.where(self.boundary_condition, neighbour_order, 0) + wrap_grid = math.where(self.boundary_condition, neighbour_order, 0) else: - wrap_grid = np.zeros(self.L.size, dtype=int) + wrap_grid = math.zeros(self.n_cells.size, dtype=int) - ranges_dim = [range(-wrap_grid[i], Lx + wrap_grid[i]) for i, Lx in enumerate(self.L)] + ranges_dim = [range(-wrap_grid[i], Lx + wrap_grid[i]) for i, Lx in enumerate(self.n_cells)] ranges_dim.append(range(n_sl)) lattice_points = [] lattice_map = [] for Lx in itertools.product(*ranges_dim): - point = np.dot(Lx[:-1], self.unit_cell) + self.basis[Lx[-1]] + point = math.dot(Lx[:-1], self.vectors) + self.positions[Lx[-1]] node_index = 0 for i in range(self.n_dim): node_index += ( - (Lx[i] % self.L[i]) * np.prod(self.L[self.n_dim - 1 - i : 0 : -1]) * n_sl + (Lx[i] % self.n_cells[i]) + * math.prod(self.n_cells[self.n_dim - 1 - i : 0 : -1]) + * n_sl ) node_index += Lx[-1] lattice_points.append(point) lattice_map.append(node_index) - return np.array(lattice_points), np.array(lattice_map) + return math.array(lattice_points), math.array(lattice_map) def add_edge(self, edge_indices): r"""Adds a specific edge based on the site index without translating it. - + Args: edge_indices: List of edges to be added. Returns: @@ -195,3 +209,137 @@ def add_edge(self, edge_indices): new_edge = edge_index self.edges.append(new_edge) + + +def _chain(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a chain lattice""" + vectors = [[1]] + n_cells = n_cells[0:1] + lattice_chain = Lattice( + n_cells=n_cells, + vectors=vectors, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + return lattice_chain + + +def _square(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a square lattice""" + vectors = [[1, 0], [0, 1]] + positions = [[0, 0]] + n_cells = n_cells[0:2] + lattice_square = Lattice( + n_cells=n_cells, + vectors=vectors, + positions=positions, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + + return lattice_square + + +def _rectangle(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a rectangle lattice""" + vectors = [[1, 0], [0, 1]] + positions = [[0, 0]] + + n_cells = n_cells[0:2] + lattice_rec = Lattice( + n_cells=n_cells, + vectors=vectors, + positions=positions, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + + return lattice_rec + + +def _honeycomb(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a honeycomb lattice""" + vectors = [[1, 0], [0.5, math.sqrt(3) / 2]] + positions = [[0, 0], [0.5, 0.5 / 3**0.5]] + + n_cells = n_cells[0:2] + lattice_honeycomb = Lattice( + n_cells=n_cells, + vectors=vectors, + positions=positions, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + + return lattice_honeycomb + + +def _triangle(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a triangular lattice""" + vectors = [[1, 0], [0.5, math.sqrt(3) / 2]] + positions = [[0, 0]] + + n_cells = n_cells[0:2] + lattice_triangle = Lattice( + n_cells=n_cells, + vectors=vectors, + positions=positions, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + + return lattice_triangle + + +def _kagome(n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates a kagome lattice""" + vectors = [[1, 0], [0.5, math.sqrt(3) / 2]] + positions = [[0.0, 0], [-0.25, math.sqrt(3) / 4], [0.25, math.sqrt(3) / 4]] + + n_cells = n_cells[0:2] + lattice_kagome = Lattice( + n_cells=n_cells, + vectors=vectors, + positions=positions, + neighbour_order=neighbour_order, + boundary_condition=boundary_condition, + ) + + return lattice_kagome + + +def _generate_lattice(lattice, n_cells, boundary_condition=False, neighbour_order=1): + r"""Generates the lattice object for given shape and n_cells. + + Args: + lattice (str): Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, ``'Triangle'``, or ``'Kagome'``. + n_cells (list[int]): Number of cells in each direction of the grid. + boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, each element represents the axis for lattice. It defaults to False. + neighbour_order (int): Range of neighbours a spin interacts with. Default is 1. + + Returns: + lattice object + """ + + lattice_shape = lattice.strip().lower() + + if lattice_shape not in ["chain", "square", "rectangle", "honeycomb", "triangle", "kagome"]: + raise ValueError( + f"Lattice shape, '{lattice}' is not supported." + f"Please set lattice to: chain, square, rectangle, honeycomb, triangle, or kagome" + ) + + if lattice_shape == "chain": + lattice = _chain(n_cells, boundary_condition, neighbour_order) + elif lattice_shape == "square": + lattice = _square(n_cells, boundary_condition, neighbour_order) + elif lattice_shape == "rectangle": + lattice = _rectangle(n_cells, boundary_condition, neighbour_order) + elif lattice_shape == "honeycomb": + lattice = _honeycomb(n_cells, boundary_condition, neighbour_order) + elif lattice_shape == "triangle": + lattice = _triangle(n_cells, boundary_condition, neighbour_order) + elif lattice_shape == "kagome": + lattice = _kagome(n_cells, boundary_condition, neighbour_order) + + return lattice diff --git a/pennylane/spin/lattice_shapes.py b/pennylane/spin/lattice_shapes.py deleted file mode 100644 index 0ad4d27c5b8..00000000000 --- a/pennylane/spin/lattice_shapes.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2018-2024 Xanadu Quantum Technologies Inc. - -# 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 - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains functions to create -:class:`~pennylane.spin.lattice` objects of different shapes. -""" - -from pennylane import numpy as np - -from .lattice import Lattice - - -def Chain(n_cells, boundary_condition=False, neighbour_order=1): - r"""Generates a chain lattice""" - unit_cell = [[1]] - L = n_cells[0:1] - lattice_chain = Lattice( - L, - unit_cell=unit_cell, - neighbour_order=neighbour_order, - boundary_condition=boundary_condition, - ) - return lattice_chain - - -def Square(n_cells, boundary_condition=False, neighbour_order=1): - r"""Generates a square lattice""" - unit_cell = [[1, 0], [0, 1]] - basis = [[0, 0]] - L = n_cells[0:2] - lattice_square = Lattice( - L=L, - unit_cell=unit_cell, - basis=basis, - neighbour_order=neighbour_order, - boundary_condition=boundary_condition, - ) - - return lattice_square - - -def Rectangle(n_cells, boundary_condition=False, neighbour_order=1): - r"""Generates a rectangle lattice""" - unit_cell = [[1, 0], [0, 1]] - basis = [[0, 0]] - - L = n_cells[0:2] - lattice_rec = Lattice( - L=L, - unit_cell=unit_cell, - basis=basis, - neighbour_order=neighbour_order, - boundary_condition=boundary_condition, - ) - - return lattice_rec - - -def Honeycomb(n_cells, boundary_condition=False, neighbour_order=1): - r"""Generates a honeycomb lattice""" - unit_cell = [[1, 0], [0.5, np.sqrt(3) / 2]] - basis = [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]] - - L = n_cells[0:2] - lattice_honeycomb = Lattice( - L=L, - unit_cell=unit_cell, - basis=basis, - neighbour_order=neighbour_order, - boundary_condition=boundary_condition, - ) - - return lattice_honeycomb - - -def Triangle(n_cells, boundary_condition=False, neighbour_order=1): - r"""Generates a triangular lattice""" - unit_cell = [[1, 0], [0.5, np.sqrt(3) / 2]] - basis = [[0, 0]] - - L = n_cells[0:2] - lattice_triangle = Lattice( - L=L, - unit_cell=unit_cell, - basis=basis, - neighbour_order=neighbour_order, - boundary_condition=boundary_condition, - ) - - return lattice_triangle diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index d48b44027e0..fd6ae01f14a 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -15,41 +15,15 @@ This file contains functions to create different templates of spin Hamiltonians. """ -from pennylane import X, Z -from pennylane import numpy as np +from pennylane import X, Z, math -from .lattice_shapes import Chain, Honeycomb, Rectangle, Square, Triangle +from .lattice import _generate_lattice # pylint: disable=too-many-arguments -def generate_lattice(lattice, n_cells, boundary_condition=False, neighbour_order=1): - r"""Generates the lattice object for given attributes.""" - - lattice_shape = lattice.strip().lower() - - if lattice_shape not in ["chain", "square", "rectangle", "honeycomb", "triangle"]: - raise ValueError( - f"Lattice shape, '{lattice}' is not supported." - f"Please set lattice to: chain, square, rectangle, honeycomb, or triangle" - ) - - if lattice_shape == "chain": - lattice = Chain(n_cells, boundary_condition, neighbour_order) - elif lattice_shape == "square": - lattice = Square(n_cells, boundary_condition, neighbour_order) - elif lattice_shape == "rectangle": - lattice = Rectangle(n_cells, boundary_condition, neighbour_order) - elif lattice_shape == "honeycomb": - lattice = Honeycomb(n_cells, boundary_condition, neighbour_order) - elif lattice_shape == "triangle": - lattice = Triangle(n_cells, boundary_condition, neighbour_order) - - return lattice - - def transverse_ising( - lattice, n_cells, coupling, h=1.0, boundary_condition=False, neighbour_order=1 + lattice, n_cells, coupling=None, h=1.0, boundary_condition=False, neighbour_order=1 ): r"""Generates the transverse field Ising model on a lattice. The Hamiltonian is represented as: @@ -61,18 +35,37 @@ def transverse_ising( magnetic field and i,j represent the indices for neighbouring spins. Args: - lattice: Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, or ``'Triangle'``. - n_cells: A list containing umber of unit cells in each direction. - coupling: Coupling between spins, it can be a constant or a 2D array of shape number of spins * number of spins. - h: Value of external magnetic field. - boundary_condition: defines boundary conditions, boolean or series of bools with dimensions same as L. - neighbour_order: Range of neighbours a spin interacts with. + lattice (str): Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, ``'Triangle'``, or ``'Kagome'``. + n_cells (list[int]): Number of cells in each direction of the grid. + coupling (List[float] or List[math.array[float]]): Coupling between spins, it can be a list of length equal to neighbour_order or a 2D array of shape number of spins * number of spins. Default value is [1.0]. + h (float): Value of external magnetic field. Default is 1.0. + boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, each element represents the axis for lattice. It defaults to False. + neighbour_order (int): Range of neighbours a spin interacts with. Default is 1. Returns: pennylane.LinearCombination: Hamiltonian for the transverse-field ising model. + + **Example** + >>> lattice = "Square" + >>> n_cells = [2,2] + >>> J = 0.5 + >>> h = 0.1 + >>> spin_ham = transverse_ising(lattice, n_cells, coupling=J, h=h) + >>> print(spin_ham) + -0.5 * (Z(0) @ Z(1)) + + -0.5 * (Z(0) @ Z(2)) + + -0.5 * (Z(1) @ Z(3)) + + -0.5 * (Z(2) @ Z(3)) + + -0.1 * X(0) + + -0.1 * X(1) + + -0.1 * X(2) + + -0.1 * X(3) + """ - lattice = generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) - coupling = np.asarray(coupling) + lattice = _generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) + if coupling is None: + coupling = [1.0] + coupling = math.asarray(coupling) hamiltonian = 0.0 print(coupling.shape) if coupling.shape not in [(neighbour_order,), (lattice.n_sites, lattice.n_sites)]: diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index ee975010674..1e75801acfb 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -7,7 +7,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, +# distributed under the License is distributed on an "AS IS" POSITIONS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. @@ -18,75 +18,76 @@ from pennylane import numpy as np from pennylane.spin import Lattice +from pennylane.spin.lattice import _generate_lattice # pylint: disable=too-many-arguments, too-many-instance-attributes def test_boundary_condition_dimension_error(): r"""Test that an error is raised if a wrong dimensions are entered for boundary_condition.""" - unit_cell = [[1]] - L = [10] + vectors = [[1]] + n_cells = [10] with pytest.raises(ValueError, match="Argument 'boundary_condition' must be a bool"): - Lattice(L=L, unit_cell=unit_cell, boundary_condition=[True, True]) + Lattice(n_cells=n_cells, vectors=vectors, boundary_condition=[True, True]) def test_boundary_condition_type_error(): r"""Test that an error is raised if a wrong type is entered for boundary_condition.""" - unit_cell = [[1]] - L = [10] + vectors = [[1]] + n_cells = [10] with pytest.raises(ValueError, match="Argument 'boundary_condition' must be a bool"): - Lattice(L=L, unit_cell=unit_cell, boundary_condition=[4]) + Lattice(n_cells=n_cells, vectors=vectors, boundary_condition=[4]) -def test_unit_cell_error(): - r"""Test that an error is raised if a wrong dimension is entered for unit_cell.""" - unit_cell = [0, 1] - L = [2, 2] +def test_vectors_error(): + r"""Test that an error is raised if a wrong dimension is entered for vectors.""" + vectors = [0, 1] + n_cells = [2, 2] with pytest.raises( - ValueError, match="'unit_cell' must have ndim==2, as array of primitive vectors." + ValueError, match="'vectors' must have ndim==2, as array of primitive vectors." ): - Lattice(L=L, unit_cell=unit_cell) + Lattice(n_cells=n_cells, vectors=vectors) -def test_basis_error(): - r"""Test that an error is raised if a wrong dimension is entered for basis.""" - unit_cell = [[0, 1], [1, 0]] - L = [2, 2] - basis = [0, 0] +def test_positions_error(): + r"""Test that an error is raised if a wrong dimension is entered for positions.""" + vectors = [[0, 1], [1, 0]] + n_cells = [2, 2] + positions = [0, 0] with pytest.raises( - ValueError, match="'basis' must have ndim==2, as array of initial coordinates." + ValueError, match="'positions' must have ndim==2, as array of initial coordinates." ): - Lattice(L=L, unit_cell=unit_cell, basis=basis) + Lattice(n_cells=n_cells, vectors=vectors, positions=positions) -def test_unit_cell_shape_error(): - r"""Test that an error is raised if a wrong dimension is entered for unit_cell.""" - unit_cell = [[0, 1, 2], [0, 1, 1]] - L = [2, 2] +def test_vectors_shape_error(): + r"""Test that an error is raised if a wrong dimension is entered for vectors.""" + vectors = [[0, 1, 2], [0, 1, 1]] + n_cells = [2, 2] with pytest.raises(ValueError, match="The number of primitive vectors must match their length"): - Lattice(L=L, unit_cell=unit_cell) + Lattice(n_cells=n_cells, vectors=vectors) -def test_L_error(): - r"""Test that an error is raised if length of unit_cell is provided in negative.""" +def test_n_cells_error(): + r"""Test that an error is raised if length of vectors is provided in negative.""" - unit_cell = [[0, 1], [1, 0]] - L = [2, -2] - with pytest.raises(TypeError, match="Argument `L` must be a list of positive integers"): - Lattice(L=L, unit_cell=unit_cell) + vectors = [[0, 1], [1, 0]] + n_cells = [2, -2] + with pytest.raises(TypeError, match="Argument `n_cells` must be a list of positive integers"): + Lattice(n_cells=n_cells, vectors=vectors) -def test_L_type_error(): - r"""Test that an error is raised if length of unit_cell is provided not as an int.""" +def test_n_cells_type_error(): + r"""Test that an error is raised if length of vectors is provided not as an int.""" - unit_cell = [[0, 1], [1, 0]] - L = [2, 2.4] - with pytest.raises(TypeError, match="Argument `L` must be a list of positive integers"): - Lattice(L=L, unit_cell=unit_cell) + vectors = [[0, 1], [1, 0]] + n_cells = [2, 2.4] + with pytest.raises(TypeError, match="Argument `n_cells` must be a list of positive integers"): + Lattice(n_cells=n_cells, vectors=vectors) @pytest.mark.parametrize( - ("unit_cell", "basis", "L"), + ("vectors", "positions", "n_cells"), [ ([[0, 1], [1, 0]], [[1.5, 1.5]], [3, 3]), ([[0, 1], [1, 0]], [[-1, -1]], [3, 3]), @@ -94,17 +95,17 @@ def test_L_type_error(): ([[1, 0], [0.5, np.sqrt(3) / 2]], [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], [2, 2]), ], ) -def test_basis(unit_cell, basis, L): - r"""Test that the lattice points start from the coordinates provided in the basis""" +def test_positions(vectors, positions, n_cells): + r"""Test that the lattice points start from the coordinates provided in the positions""" - lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis) - for i, b in enumerate(basis): + lattice = Lattice(n_cells=n_cells, vectors=vectors, positions=positions) + for i, b in enumerate(positions): assert np.allclose(b, lattice.lattice_points[i]) @pytest.mark.parametrize( - ("unit_cell", "basis", "L", "expected_number"), - # expected_number here was obtained manually + ("vectors", "positions", "n_cells", "expected_number"), + # expected_number here was obtained manually. [ ([[0, 1], [1, 0]], [[0, 0]], [3, 3], 9), ([[0, 1], [1, 0]], [[0, 0]], [6, 7], 42), @@ -112,15 +113,15 @@ def test_basis(unit_cell, basis, L): (np.eye(3), None, [3, 3, 4], 36), ], ) -def test_lattice_points(unit_cell, basis, L, expected_number): +def test_lattice_points(vectors, positions, n_cells, expected_number): r"""Test that the correct number of lattice points are generated for the given attributes""" - lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis) + lattice = Lattice(n_cells=n_cells, vectors=vectors, positions=positions) assert len(lattice.lattice_points == expected_number) @pytest.mark.parametrize( - # expected_edges here were obtained with netket. - ("unit_cell", "basis", "L", "boundary_condition", "expected_edges"), + # expected_edges here were obtained manually. + ("vectors", "positions", "n_cells", "boundary_condition", "expected_edges"), [ ( [[0, 1], [1, 0]], @@ -199,15 +200,17 @@ def test_lattice_points(unit_cell, basis, L, expected_number): ), ], ) -def test_boundary_condition(unit_cell, basis, L, boundary_condition, expected_edges): +def test_boundary_condition(vectors, positions, n_cells, boundary_condition, expected_edges): r"""Test that the correct edges are obtained for given boundary conditions""" - lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis, boundary_condition=boundary_condition) + lattice = Lattice( + n_cells=n_cells, vectors=vectors, positions=positions, boundary_condition=boundary_condition + ) assert sorted(lattice.edges) == sorted(expected_edges) @pytest.mark.parametrize( - # expected_edges here were obtained with netket. - ("unit_cell", "basis", "L", "neighbour_order", "expected_edges"), + # expected_edges here were obtained manually. + ("vectors", "positions", "n_cells", "neighbour_order", "expected_edges"), [ ( [[0, 1], [1, 0]], @@ -218,23 +221,23 @@ def test_boundary_condition(unit_cell, basis, L, boundary_condition, expected_ed (0, 1, 0), (1, 2, 0), (3, 4, 0), - (5, 8, 0), - (0, 3, 0), - (1, 4, 0), - (6, 7, 0), (4, 5, 0), + (6, 7, 0), + (7, 8, 0), + (0, 3, 0), (3, 6, 0), - (2, 5, 0), + (1, 4, 0), (4, 7, 0), - (7, 8, 0), + (2, 5, 0), + (5, 8, 0), (2, 4, 1), + (1, 3, 1), + (5, 7, 1), + (4, 6, 1), (0, 4, 1), (1, 5, 1), (3, 7, 1), - (4, 6, 1), - (5, 7, 1), (4, 8, 1), - (1, 3, 1), ], ), ( @@ -244,57 +247,59 @@ def test_boundary_condition(unit_cell, basis, L, boundary_condition, expected_ed 3, [ (0, 1, 0), - (9, 10, 0), (1, 2, 0), - (0, 4, 0), - (10, 11, 0), - (1, 5, 0), - (3, 7, 0), (2, 3, 0), - (6, 7, 0), (4, 5, 0), - (8, 9, 0), - (2, 6, 0), (5, 6, 0), + (6, 7, 0), + (8, 9, 0), + (9, 10, 0), + (10, 11, 0), + (0, 4, 0), (4, 8, 0), - (6, 10, 0), + (1, 5, 0), (5, 9, 0), + (2, 6, 0), + (6, 10, 0), + (3, 7, 0), (7, 11, 0), - (2, 7, 1), - (5, 8, 1), + (0, 5, 1), (4, 9, 1), + (1, 6, 1), + (5, 10, 1), + (2, 7, 1), (6, 11, 1), - (7, 10, 1), (1, 4, 1), - (5, 10, 1), - (0, 5, 1), - (3, 6, 1), - (1, 6, 1), + (5, 8, 1), (2, 5, 1), (6, 9, 1), + (3, 6, 1), + (7, 10, 1), + (0, 8, 2), + (1, 9, 2), (2, 10, 2), + (3, 11, 2), + (0, 2, 2), (4, 6, 2), (8, 10, 2), + (1, 3, 2), (5, 7, 2), - (0, 2, 2), (9, 11, 2), - (0, 8, 2), - (1, 3, 2), - (1, 9, 2), - (3, 11, 2), ], ), ([[1, 0], [0.5, np.sqrt(3) / 2]], [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], [2, 2], 0, []), ], ) -def test_neighbour_order(unit_cell, basis, L, neighbour_order, expected_edges): +def test_neighbour_order(vectors, positions, n_cells, neighbour_order, expected_edges): r"""Test that the correct edges are obtained for given neighbour order""" - lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis, neighbour_order=neighbour_order) + lattice = Lattice( + n_cells=n_cells, vectors=vectors, positions=positions, neighbour_order=neighbour_order + ) assert sorted(lattice.edges) == sorted(expected_edges) @pytest.mark.parametrize( - ("unit_cell", "basis", "L", "boundary_condition", "n_dim", "expected_bc"), + ("vectors", "positions", "n_cells", "boundary_condition", "n_dim", "expected_bc"), [ ([[0, 1], [1, 0]], [[1.5, 1.5]], [3, 3], True, 2, [True, True]), ([[0, 1], [1, 0]], [[-1, -1]], [3, 3], False, 2, [False, False]), @@ -310,12 +315,14 @@ def test_neighbour_order(unit_cell, basis, L, neighbour_order, expected_edges): (np.eye(3), [[0, 0, 0]], [3, 3, 4], True, 3, [True, True, True]), ], ) -def test_attributes(unit_cell, basis, L, boundary_condition, n_dim, expected_bc): +def test_attributes(vectors, positions, n_cells, boundary_condition, n_dim, expected_bc): r"""Test that the methods and attributes return correct values""" - lattice = Lattice(L=L, unit_cell=unit_cell, basis=basis, boundary_condition=boundary_condition) + lattice = Lattice( + n_cells=n_cells, vectors=vectors, positions=positions, boundary_condition=boundary_condition + ) - assert np.all(lattice.unit_cell == unit_cell) - assert np.all(lattice.basis == basis) + assert np.all(lattice.vectors == vectors) + assert np.all(lattice.positions == positions) assert lattice.n_dim == n_dim assert np.all(lattice.boundary_condition == expected_bc) @@ -323,9 +330,9 @@ def test_attributes(unit_cell, basis, L, boundary_condition, n_dim, expected_bc) def test_add_edge_error(): r"""Test that an error is raised if the added edge is already present for a lattice""" edge_indices = [[4, 5]] - unit_cell = [[0, 1], [1, 0]] - L = [3, 3] - lattice = Lattice(L=L, unit_cell=unit_cell) + vectors = [[0, 1], [1, 0]] + n_cells = [3, 3] + lattice = Lattice(n_cells=n_cells, vectors=vectors) with pytest.raises(ValueError, match="Edge is already present"): lattice.add_edge(edge_indices) @@ -334,9 +341,119 @@ def test_add_edge_error(): def test_add_edge(): r"""Test that edges are added per their index to a lattice""" edge_indices = [[1, 3], [4, 6]] - unit_cell = [[0, 1], [1, 0]] - L = [3, 3] - lattice = Lattice(L=L, unit_cell=unit_cell) + vectors = [[0, 1], [1, 0]] + n_cells = [3, 3] + lattice = Lattice(n_cells=n_cells, vectors=vectors) lattice.add_edge(edge_indices) assert np.all(np.isin(edge_indices, lattice.edges)) + + +@pytest.mark.parametrize( + # expected_edges here were obtained with manually. + ("shape", "n_cells", "expected_edges"), + [ + ( + "chAin ", + [10, 0, 0], + [ + (0, 1, 0), + (1, 2, 0), + (3, 4, 0), + (2, 3, 0), + (6, 7, 0), + (4, 5, 0), + (8, 9, 0), + (5, 6, 0), + (7, 8, 0), + ], + ), + ( + "Square", + [3, 3], + [ + (0, 1, 0), + (1, 2, 0), + (3, 4, 0), + (4, 5, 0), + (6, 7, 0), + (7, 8, 0), + (0, 3, 0), + (3, 6, 0), + (1, 4, 0), + (4, 7, 0), + (2, 5, 0), + (5, 8, 0), + ], + ), + ( + " Rectangle ", + [3, 4], + [ + (0, 1, 0), + (1, 2, 0), + (2, 3, 0), + (4, 5, 0), + (5, 6, 0), + (6, 7, 0), + (8, 9, 0), + (9, 10, 0), + (10, 11, 0), + (0, 4, 0), + (4, 8, 0), + (1, 5, 0), + (5, 9, 0), + (2, 6, 0), + (6, 10, 0), + (3, 7, 0), + (7, 11, 0), + ], + ), + ( + "honeycomb", + [2, 2], + [ + (0, 1, 0), + (1, 2, 0), + (2, 3, 0), + (3, 6, 0), + (6, 7, 0), + (5, 6, 0), + (4, 5, 0), + (1, 4, 0), + ], + ), + ( + "TRIANGLE", + [2, 2], + [(0, 1, 0), (1, 2, 0), (2, 3, 0), (0, 2, 0), (1, 3, 0)], + ), + ( + "Kagome", + [2, 2], + [ + (0, 1, 0), + (1, 2, 0), + (0, 2, 0), + (3, 4, 0), + (3, 5, 0), + (4, 5, 0), + (6, 7, 0), + (6, 8, 0), + (7, 8, 0), + (9, 10, 0), + (9, 11, 0), + (10, 11, 0), + (2, 3, 0), + (2, 7, 0), + (3, 7, 0), + (5, 10, 0), + (8, 9, 0), + ], + ), + ], +) +def test_edges_for_shapes(shape, n_cells, expected_edges): + r"""Test that correct edges are obtained for given lattice shapes""" + lattice = _generate_lattice(lattice=shape, n_cells=n_cells) + assert sorted(lattice.edges) == sorted(expected_edges) diff --git a/tests/spin/test_lattice_shapes.py b/tests/spin/test_lattice_shapes.py deleted file mode 100644 index 4d63c454ba3..00000000000 --- a/tests/spin/test_lattice_shapes.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2018-2024 Xanadu Quantum Technologies Inc. - -# 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 - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for functions needed for computing the lattices of certain shapes. -""" - -import pytest - -from pennylane.spin import generate_lattice - - -@pytest.mark.parametrize( - # expected_edges here were obtained with netket. - ("shape", "L", "expected_edges"), - [ - ( - "chAin ", - [10, 0, 0], - [ - (0, 1, 0), - (1, 2, 0), - (3, 4, 0), - (2, 3, 0), - (6, 7, 0), - (4, 5, 0), - (8, 9, 0), - (5, 6, 0), - (7, 8, 0), - ], - ), - ( - "Square", - [3, 3], - [ - (0, 1, 0), - (1, 2, 0), - (3, 4, 0), - (5, 8, 0), - (0, 3, 0), - (1, 4, 0), - (6, 7, 0), - (4, 5, 0), - (3, 6, 0), - (2, 5, 0), - (4, 7, 0), - (7, 8, 0), - ], - ), - ( - " Rectangle ", - [3, 4], - [ - (0, 1, 0), - (9, 10, 0), - (1, 2, 0), - (0, 4, 0), - (10, 11, 0), - (1, 5, 0), - (3, 7, 0), - (2, 3, 0), - (6, 7, 0), - (4, 5, 0), - (8, 9, 0), - (2, 6, 0), - (5, 6, 0), - (4, 8, 0), - (6, 10, 0), - (5, 9, 0), - (7, 11, 0), - ], - ), - ( - "honeycomb", - [2, 2], - [ - (0, 1, 0), - (1, 2, 0), - (1, 4, 0), - (2, 3, 0), - (6, 7, 0), - (4, 5, 0), - (5, 6, 0), - (3, 6, 0), - ], - ), - ( - "TRIANGLE", - [2, 2], - [(0, 1, 0), (1, 2, 0), (2, 3, 0), (0, 2, 0), (1, 3, 0)], - ), - ], -) -def test_edges_for_shapes(shape, L, expected_edges): - r"""Test that correct edges are obtained for given lattice shapes""" - lattice = generate_lattice(lattice=shape, n_cells=L) - assert sorted(lattice.edges) == sorted(expected_edges) diff --git a/tests/spin/test_spin_hamiltonian.py b/tests/spin/test_spin_hamiltonian.py index 3299aae1faf..0062d238bcf 100644 --- a/tests/spin/test_spin_hamiltonian.py +++ b/tests/spin/test_spin_hamiltonian.py @@ -18,6 +18,7 @@ import pytest import pennylane as qml +from pennylane import X, Z from pennylane.spin import transverse_ising @@ -38,24 +39,114 @@ def test_shape_error(): @pytest.mark.parametrize( - ("shape_ds", "shape_lattice", "layout", "n_cells"), + # expected_ham here was obtained from datasets + ("shape", "n_cells", "h", "expected_ham"), [ - ("chain", "chain", "1x4", [4, 0, 0]), - ("chain", "chain", "1x8", [8, 0, 0]), - ("rectangular", "rectangle", "2x4", [4, 2, 0]), - ("rectangular", "rectangle", "2x8", [8, 2, 0]), + ( + "chain", + [4, 0, 0], + 0, + -1.0 * (Z(0) @ Z(1)) + + -1.0 * (Z(1) @ Z(2)) + + -1.0 * (Z(2) @ Z(3)) + + 0.0 * X(0) + + 0.0 * X(1) + + 0.0 * X(2) + + 0.0 * X(3), + ), + ( + "chain", + [8, 0, 0], + -0.17676768, + -1.0 * (Z(0) @ Z(1)) + + -1.0 * (Z(1) @ Z(2)) + + -1.0 * (Z(2) @ Z(3)) + + -1.0 * (Z(3) @ Z(4)) + + -1.0 * (Z(4) @ Z(5)) + + -1.0 * (Z(5) @ Z(6)) + + -1.0 * (Z(6) @ Z(7)) + + 0.17676767676767677 * X(0) + + 0.17676767676767677 * X(1) + + 0.17676767676767677 * X(2) + + 0.17676767676767677 * X(3) + + 0.17676767676767677 * X(4) + + 0.17676767676767677 * X(5) + + 0.17676767676767677 * X(6) + + 0.17676767676767677 * X(7), + ), + ( + "rectangle", + [4, 2, 0], + -0.25252525, + -1.0 * (Z(0) @ Z(1)) + + -1.0 * (Z(0) @ Z(2)) + + -1.0 * (Z(2) @ Z(3)) + + -1.0 * (Z(2) @ Z(4)) + + -1.0 * (Z(4) @ Z(5)) + + -1.0 * (Z(4) @ Z(6)) + + -1.0 * (Z(6) @ Z(7)) + + -1.0 * (Z(1) @ Z(3)) + + -1.0 * (Z(3) @ Z(5)) + + -1.0 * (Z(5) @ Z(7)) + + 0.25252525252525254 * X(0) + + 0.25252525252525254 * X(1) + + 0.25252525252525254 * X(2) + + 0.25252525252525254 * X(3) + + 0.25252525252525254 * X(4) + + 0.25252525252525254 * X(5) + + 0.25252525252525254 * X(6) + + 0.25252525252525254 * X(7), + ), + ( + "rectangle", + [8, 2, 0], + -0.44444444, + -1.0 * (Z(0) @ Z(1)) + + -1.0 * (Z(0) @ Z(2)) + + -1.0 * (Z(2) @ Z(3)) + + -1.0 * (Z(2) @ Z(4)) + + -1.0 * (Z(4) @ Z(5)) + + -1.0 * (Z(4) @ Z(6)) + + -1.0 * (Z(6) @ Z(7)) + + -1.0 * (Z(6) @ Z(8)) + + -1.0 * (Z(8) @ Z(9)) + + -1.0 * (Z(8) @ Z(10)) + + -1.0 * (Z(10) @ Z(11)) + + -1.0 * (Z(10) @ Z(12)) + + -1.0 * (Z(12) @ Z(13)) + + -1.0 * (Z(12) @ Z(14)) + + -1.0 * (Z(14) @ Z(15)) + + -1.0 * (Z(1) @ Z(3)) + + -1.0 * (Z(3) @ Z(5)) + + -1.0 * (Z(5) @ Z(7)) + + -1.0 * (Z(7) @ Z(9)) + + -1.0 * (Z(9) @ Z(11)) + + -1.0 * (Z(11) @ Z(13)) + + -1.0 * (Z(13) @ Z(15)) + + 0.4444444444444444 * X(0) + + 0.4444444444444444 * X(1) + + 0.4444444444444444 * X(2) + + 0.4444444444444444 * X(3) + + 0.4444444444444444 * X(4) + + 0.4444444444444444 * X(5) + + 0.4444444444444444 * X(6) + + 0.4444444444444444 * X(7) + + 0.4444444444444444 * X(8) + + 0.4444444444444444 * X(9) + + 0.4444444444444444 * X(10) + + 0.4444444444444444 * X(11) + + 0.4444444444444444 * X(12) + + 0.4444444444444444 * X(13) + + 0.4444444444444444 * X(14) + + 0.4444444444444444 * X(15), + ), ], ) -def test_ising_hamiltonian(shape_ds, shape_lattice, layout, n_cells): +def test_ising_hamiltonian(shape, n_cells, h, expected_ham): r"""Test that the correct Hamiltonian is generated compared to the datasets""" - spin_dataset = qml.data.load("qspin", sysname="Ising", lattice=shape_ds, layout=layout) - dataset_ham = spin_dataset[0].hamiltonians[0] - J = [-spin_dataset[0].parameters["J"]] - h = -spin_dataset[0].parameters["h"][0] + J = [1.0] - ising_ham = transverse_ising( - lattice=shape_lattice, n_cells=n_cells, coupling=J, h=h, neighbour_order=1 - ) + ising_ham = transverse_ising(lattice=shape, n_cells=n_cells, coupling=J, h=h, neighbour_order=1) - assert qml.equal(ising_ham, dataset_ham) + assert qml.equal(ising_ham, expected_ham) From 932b8230e12ba628fe802a11d824078156eba6d9 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Tue, 20 Aug 2024 13:15:28 -0400 Subject: [PATCH 12/38] [skip ci] updated change-log --- doc/releases/changelog-dev.md | 234 ++++++++++++++++++++-------------- 1 file changed, 135 insertions(+), 99 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index a4a11dcef0b..4b97a39764f 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -4,42 +4,27 @@

New features since last release

-* Mid-circuit measurements can now be captured with `qml.capture` enabled. - [(#6015)](https://github.com/PennyLaneAI/pennylane/pull/6015) - -* A new method `process_density_matrix` has been added to the `ProbabilityMP` and `DensityMatrixMP` - classes, allowing for more efficient handling of quantum density matrices, particularly with batch - processing support. This method simplifies the calculation of probabilities from quantum states - represented as density matrices. - [(#5830)](https://github.com/PennyLaneAI/pennylane/pull/5830) - -* The `qml.PrepSelPrep` template is added. The template implements a block-encoding of a linear - combination of unitaries. - [(#5756)](https://github.com/PennyLaneAI/pennylane/pull/5756) - [(#5987)](https://github.com/PennyLaneAI/pennylane/pull/5987) +

Converting noise models from Qiskit ♻️

* A new `qml.from_qiskit_noise` method now allows one to convert a Qiskit ``NoiseModel`` to a PennyLane ``NoiseModel`` via the Pennylane-Qiskit plugin. [(#5996)](https://github.com/PennyLaneAI/pennylane/pull/5996) -* A new function `qml.registers` has been added, enabling the creation of registers, which are implemented as a dictionary of `Wires` instances. - [(#5957)](https://github.com/PennyLaneAI/pennylane/pull/5957) +

Registers of wires 🌈

-* The `split_to_single_terms` transform is added. This transform splits expectation values of sums - into multiple single-term measurements on a single tape, providing better support for simulators - that can handle non-commuting observables but don't natively support multi-term observables. - [(#5884)](https://github.com/PennyLaneAI/pennylane/pull/5884) +* Set operations are now supported by Wires. + [(#5983)](https://github.com/PennyLaneAI/pennylane/pull/5983) -* `SProd.terms` now flattens out the terms if the base is a multi-term observable. - [(#5885)](https://github.com/PennyLaneAI/pennylane/pull/5885) +* The representation for `Wires` has now changed to be more copy-paste friendly. + [(#5958)](https://github.com/PennyLaneAI/pennylane/pull/5958) -* A new method `to_mat` has been added to the `FermiWord` and `FermiSentence` classes, which allows - computing the matrix representation of these Fermi operators. - [(#5920)](https://github.com/PennyLaneAI/pennylane/pull/5920) +* A new function `qml.registers` has been added, enabling the creation of registers, which are implemented as a dictionary of `Wires` instances. + [(#5957)](https://github.com/PennyLaneAI/pennylane/pull/5957) + [(#6102)](https://github.com/PennyLaneAI/pennylane/pull/6102) -* New functionality has been added to natively support exponential extrapolation when using the `mitigate_with_zne`. This allows - users to have more control over the error mitigation protocol without needing to add further dependencies. - [(#5972)](https://github.com/PennyLaneAI/pennylane/pull/5972) +

Quantum arithmetic operations 🧮

+ +

Creating spin Hamiltonians 🧑‍🎨

* New function to generate transverse-field Ising Hamiltonian and the required helper functions to generate the lattice have been added. @@ -47,48 +32,96 @@

Improvements 🛠

-* `QNGOptimizer` now supports cost functions with multiple arguments, updating each argument independently. - [(#5926)](https://github.com/PennyLaneAI/pennylane/pull/5926) +

A Prep-Select-Prep template

-* `qml.for_loop` can now be captured into plxpr. - [(#6041)](https://github.com/PennyLaneAI/pennylane/pull/6041) - [(#6064)](https://github.com/PennyLaneAI/pennylane/pull/6064) +* The `qml.PrepSelPrep` template is added. The template implements a block-encoding of a linear + combination of unitaries. + [(#5756)](https://github.com/PennyLaneAI/pennylane/pull/5756) + [(#5987)](https://github.com/PennyLaneAI/pennylane/pull/5987) -* `qml.for_loop` now supports `range`-like syntax with default `step=1`. - [(#6068)](https://github.com/PennyLaneAI/pennylane/pull/6068) +

QChem improvements

-* Removed `semantic_version` from the list of required packages in PennyLane. - [(#5836)](https://github.com/PennyLaneAI/pennylane/pull/5836) +* Molecules and Hamiltonians can now be constructed for all the elements present in the periodic table. + [(#5821)](https://github.com/PennyLaneAI/pennylane/pull/5821) -* Added the `compute_decomposition` method for `qml.Hermitian`. - [(#6062)](https://github.com/PennyLaneAI/pennylane/pull/6062) +* `qml.UCCSD` now accepts an additional optional argument, `n_repeats`, which defines the number of + times the UCCSD template is repeated. This can improve the accuracy of the template by reducing + the Trotter error but would result in deeper circuits. + [(#5801)](https://github.com/PennyLaneAI/pennylane/pull/5801) -* During experimental program capture, the qnode can now use closure variables. - [(#6052)](https://github.com/PennyLaneAI/pennylane/pull/6052) +* The `qubit_observable` function is modified to return an ascending wire order for molecular + Hamiltonians. + [(#5950)](https://github.com/PennyLaneAI/pennylane/pull/5950) -* `GlobalPhase` now supports parameter broadcasting. - [(#5923)](https://github.com/PennyLaneAI/pennylane/pull/5923) +* A new method `to_mat` has been added to the `FermiWord` and `FermiSentence` classes, which allows + computing the matrix representation of these Fermi operators. + [(#5920)](https://github.com/PennyLaneAI/pennylane/pull/5920) -* `qml.devices.LegacyDeviceFacade` has been added to map the legacy devices to the new - device interface. - [(#5927)](https://github.com/PennyLaneAI/pennylane/pull/5927) +

Improvements to operators

-* Added the `compute_sparse_matrix` method for `qml.ops.qubit.BasisStateProjector`. - [(#5790)](https://github.com/PennyLaneAI/pennylane/pull/5790) +* `GlobalPhase` now supports parameter broadcasting. + [(#5923)](https://github.com/PennyLaneAI/pennylane/pull/5923) -* `StateMP.process_state` defines rules in `cast_to_complex` for complex casting, avoiding a superfluous state vector copy in Lightning simulations - [(#5995)](https://github.com/PennyLaneAI/pennylane/pull/5995) +* Added the `compute_decomposition` method for `qml.Hermitian`. + [(#6062)](https://github.com/PennyLaneAI/pennylane/pull/6062) * Port the fast `apply_operation` implementation of `PauliZ` to `PhaseShift`, `S` and `T`. [(#5876)](https://github.com/PennyLaneAI/pennylane/pull/5876) -* `qml.UCCSD` now accepts an additional optional argument, `n_repeats`, which defines the number of - times the UCCSD template is repeated. This can improve the accuracy of the template by reducing - the Trotter error but would result in deeper circuits. - [(#5801)](https://github.com/PennyLaneAI/pennylane/pull/5801) +* The `CNOT` operator no longer decomposes to itself. Instead, it raises a `qml.DecompositionUndefinedError`. + [(#6039)](https://github.com/PennyLaneAI/pennylane/pull/6039) -* `QuantumScript.hash` is now cached, leading to performance improvements. - [(#5919)](https://github.com/PennyLaneAI/pennylane/pull/5919) +

Mid-circuit measurement improvements

+ +* `qml.dynamic_one_shot` now supports circuits using the `"tensorflow"` interface. + [(#5973)](https://github.com/PennyLaneAI/pennylane/pull/5973) + +* If the conditional does not include a mid-circuit measurement, then `qml.cond` + will automatically evaluate conditionals using standard Python control flow. + [(#6016)](https://github.com/PennyLaneAI/pennylane/pull/6016) + + This allows `qml.cond` to be used to represent a wider range of conditionals: + + ```python + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(x): + c = qml.cond(x > 2.7, qml.RX, qml.RZ) + c(x, wires=0) + return qml.probs(wires=0) + ``` + + ```pycon + >>> print(qml.draw(circuit)(3.8)) + 0: ──RX(3.80)─┤ Probs + >>> print(qml.draw(circuit)(0.54)) + 0: ──RZ(0.54)─┤ Probs + ``` + +

Transforms

+ +* The `diagonalize_measurements` transform is added. This transform converts measurements + to the Z basis by applying the relevant diagonalizing gates. It can be set to diagonalize only + a subset of the base observables `{X, Y, Z, Hadamard}`. + [(#5829)](https://github.com/PennyLaneAI/pennylane/pull/5829) + +* The `split_to_single_terms` transform is added. This transform splits expectation values of sums + into multiple single-term measurements on a single tape, providing better support for simulators + that can handle non-commuting observables but don't natively support multi-term observables. + [(#5884)](https://github.com/PennyLaneAI/pennylane/pull/5884) + +* New functionality has been added to natively support exponential extrapolation when using the `mitigate_with_zne`. This allows + users to have more control over the error mitigation protocol without needing to add further dependencies. + [(#5972)](https://github.com/PennyLaneAI/pennylane/pull/5972) + +* `fuse_rot_angles` now respects the global phase of the combined rotations. + [(#6031)](https://github.com/PennyLaneAI/pennylane/pull/6031) + +

Capturing and representing hybrid programs

+ +* `qml.for_loop` now supports `range`-like syntax with default `step=1`. + [(#6068)](https://github.com/PennyLaneAI/pennylane/pull/6068) * Applying `adjoint` and `ctrl` to a quantum function can now be captured into plxpr. Furthermore, the `qml.cond` function can be captured into plxpr. @@ -100,20 +133,15 @@ * During experimental program capture, functions that accept and/or return `pytree` structures can now be handled in the `QNode` call, `cond`, `for_loop` and `while_loop`. [(#6081)](https://github.com/PennyLaneAI/pennylane/pull/6081) -* Set operations are now supported by Wires. - [(#5983)](https://github.com/PennyLaneAI/pennylane/pull/5983) - -* `qml.dynamic_one_shot` now supports circuits using the `"tensorflow"` interface. - [(#5973)](https://github.com/PennyLaneAI/pennylane/pull/5973) - -* The representation for `Wires` has now changed to be more copy-paste friendly. - [(#5958)](https://github.com/PennyLaneAI/pennylane/pull/5958) +* During experimental program capture, the qnode can now use closure variables. + [(#6052)](https://github.com/PennyLaneAI/pennylane/pull/6052) -* Observable validation for `default.qubit` is now based on execution mode (analytic vs. finite shots) and measurement type (sample measurement vs. state measurement). - [(#5890)](https://github.com/PennyLaneAI/pennylane/pull/5890) +* Mid-circuit measurements can now be captured with `qml.capture` enabled. + [(#6015)](https://github.com/PennyLaneAI/pennylane/pull/6015) -* Molecules and Hamiltonians can now be constructed for all the elements present in the periodic table. - [(#5821)](https://github.com/PennyLaneAI/pennylane/pull/5821) +* `qml.for_loop` can now be captured into plxpr. + [(#6041)](https://github.com/PennyLaneAI/pennylane/pull/6041) + [(#6064)](https://github.com/PennyLaneAI/pennylane/pull/6064) * `qml.for_loop` and `qml.while_loop` now fallback to standard Python control flow if `@qjit` is not present, allowing the same code to work with and without @@ -160,36 +188,6 @@ 0.11917543, 0.08942104, 0.21545687], dtype=float64) ``` -* If the conditional does not include a mid-circuit measurement, then `qml.cond` - will automatically evaluate conditionals using standard Python control flow. - [(#6016)](https://github.com/PennyLaneAI/pennylane/pull/6016) - - This allows `qml.cond` to be used to represent a wider range of conditionals: - - ```python - dev = qml.device("default.qubit", wires=1) - - @qml.qnode(dev) - def circuit(x): - c = qml.cond(x > 2.7, qml.RX, qml.RZ) - c(x, wires=0) - return qml.probs(wires=0) - ``` - - ```pycon - >>> print(qml.draw(circuit)(3.8)) - 0: ──RX(3.80)─┤ Probs - >>> print(qml.draw(circuit)(0.54)) - 0: ──RZ(0.54)─┤ Probs - ``` - -* The `qubit_observable` function is modified to return an ascending wire order for molecular - Hamiltonians. - [(#5950)](https://github.com/PennyLaneAI/pennylane/pull/5950) - -* The `CNOT` operator no longer decomposes to itself. Instead, it raises a `qml.DecompositionUndefinedError`. - [(#6039)](https://github.com/PennyLaneAI/pennylane/pull/6039) -

Community contributions 🥳

* Resolved the bug in `qml.ThermalRelaxationError` where there was a typo from `tq` to `tg`. @@ -199,7 +197,40 @@ `readout_misclassification_probs` on the `default.qutrit.mixed` device. These parameters add a `~.QutritAmplitudeDamping` and a `~.TritFlip` channel, respectively, after measurement diagonalization. The amplitude damping error represents the potential for relaxation to occur during longer measurements. The trit flip error represents misclassification during readout. - [(#5842)](https://github.com/PennyLaneAI/pennylane/pull/5842) + [(#5842)](https://github.com/PennyLaneAI/pennylane/pull/5842)s + +

Other improvements

+ +* A new method `process_density_matrix` has been added to the `ProbabilityMP` and `DensityMatrixMP` + classes, allowing for more efficient handling of quantum density matrices, particularly with batch + processing support. This method simplifies the calculation of probabilities from quantum states + represented as density matrices. + [(#5830)](https://github.com/PennyLaneAI/pennylane/pull/5830) + +* `SProd.terms` now flattens out the terms if the base is a multi-term observable. + [(#5885)](https://github.com/PennyLaneAI/pennylane/pull/5885) + +* `QNGOptimizer` now supports cost functions with multiple arguments, updating each argument independently. + [(#5926)](https://github.com/PennyLaneAI/pennylane/pull/5926) + +* Removed `semantic_version` from the list of required packages in PennyLane. + [(#5836)](https://github.com/PennyLaneAI/pennylane/pull/5836) + +* `qml.devices.LegacyDeviceFacade` has been added to map the legacy devices to the new + device interface. + [(#5927)](https://github.com/PennyLaneAI/pennylane/pull/5927) + +* Added the `compute_sparse_matrix` method for `qml.ops.qubit.BasisStateProjector`. + [(#5790)](https://github.com/PennyLaneAI/pennylane/pull/5790) + +* `StateMP.process_state` defines rules in `cast_to_complex` for complex casting, avoiding a superfluous state vector copy in Lightning simulations + [(#5995)](https://github.com/PennyLaneAI/pennylane/pull/5995) + +* `QuantumScript.hash` is now cached, leading to performance improvements. + [(#5919)](https://github.com/PennyLaneAI/pennylane/pull/5919) + +* Observable validation for `default.qubit` is now based on execution mode (analytic vs. finite shots) and measurement type (sample measurement vs. state measurement). + [(#5890)](https://github.com/PennyLaneAI/pennylane/pull/5890)

Breaking changes 💔

@@ -290,6 +321,9 @@

Bug fixes 🐛

+* `fuse_rot_angles` no longer returns wrong derivatives at singular points but returns NaN. + [(#6031)](https://github.com/PennyLaneAI/pennylane/pull/6031) + * `qml.GlobalPhase` and `qml.I` can now be captured when acting on no wires. [(#6060)](https://github.com/PennyLaneAI/pennylane/pull/6060) @@ -334,6 +368,9 @@ * Fixes a bug where `CompositeOp.overlapping_ops` changes the original ordering of ops, causing incorrect matrix generated for `Prod` with `Sum` as operands. [(#6091)](https://github.com/PennyLaneAI/pennylane/pull/6091) +* `qml.qsvt` now works with "Wx" convention and any number of angles. + [(#6105)](https://github.com/PennyLaneAI/pennylane/pull/6105) +

Contributors ✍️

This release contains contributions from (in alphabetical order): @@ -346,7 +383,6 @@ Ahmed Darwish, Astral Cai, Yushao Chen, Ahmed Darwish, -Diksha Dhawan, Maja Franz, Lillian M. A. Frederiksen, Pietropaolo Frisoni, From 7bae9d297e331a6be5f3f257d69a72f91e23de53 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Tue, 20 Aug 2024 13:52:26 -0400 Subject: [PATCH 13/38] [skip ci] Fixed a test --- tests/spin/test_lattice.py | 75 ++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index 1e75801acfb..a0b3788c018 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -87,20 +87,79 @@ def test_n_cells_type_error(): @pytest.mark.parametrize( - ("vectors", "positions", "n_cells"), + # expected_points here were calculated manually. + ("vectors", "positions", "n_cells", "expected_points"), [ - ([[0, 1], [1, 0]], [[1.5, 1.5]], [3, 3]), - ([[0, 1], [1, 0]], [[-1, -1]], [3, 3]), - ([[0, 1], [1, 0]], [[10, 10]], [3, 3]), - ([[1, 0], [0.5, np.sqrt(3) / 2]], [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], [2, 2]), + ( + [[0, 1], [1, 0]], + [[1.5, 1.5]], + [3, 3], + [ + [1.5, 1.5], + [2.5, 1.5], + [3.5, 1.5], + [1.5, 2.5], + [2.5, 2.5], + [3.5, 2.5], + [1.5, 3.5], + [2.5, 3.5], + [3.5, 3.5], + ], + ), + ( + [[0, 1], [1, 0]], + [[-1, -1]], + [3, 3], + [ + [-1.0, -1.0], + [0.0, -1.0], + [1.0, -1.0], + [-1.0, 0.0], + [0.0, 0.0], + [1.0, 0.0], + [-1.0, 1.0], + [0.0, 1.0], + [1.0, 1.0], + ], + ), + ( + [[0, 1], [1, 0]], + [[10, 10]], + [3, 3], + [ + [10.0, 10.0], + [11.0, 10.0], + [12.0, 10.0], + [10.0, 11.0], + [11.0, 11.0], + [12.0, 11.0], + [10.0, 12.0], + [11.0, 12.0], + [12.0, 12.0], + ], + ), + ( + [[1, 0], [0.5, np.sqrt(3) / 2]], + [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], + [2, 2], + [ + [0.5, 0.28867513], + [1.0, 0.57735027], + [1.0, 1.15470054], + [1.5, 1.44337567], + [1.5, 0.28867513], + [2.0, 0.57735027], + [2.0, 1.15470054], + [2.5, 1.44337567], + ], + ), ], ) -def test_positions(vectors, positions, n_cells): +def test_positions(vectors, positions, n_cells, expected_points): r"""Test that the lattice points start from the coordinates provided in the positions""" lattice = Lattice(n_cells=n_cells, vectors=vectors, positions=positions) - for i, b in enumerate(positions): - assert np.allclose(b, lattice.lattice_points[i]) + assert np.allclose(expected_points, lattice.lattice_points) @pytest.mark.parametrize( From b89147dd3c6474b65102416b4e966430a85a696b Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:48:13 -0400 Subject: [PATCH 14/38] Update pennylane/spin/lattice.py Co-authored-by: Utkarsh --- pennylane/spin/lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 6da346b4760..07186636d69 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -82,7 +82,7 @@ def __init__( raise ValueError("'positions' must have ndim==2, as array of initial coordinates.") if isinstance(boundary_condition, bool): - boundary_condition = [boundary_condition for _ in range(len(n_cells))] + boundary_condition = [boundary_condition] * len(n_cells) if not all(isinstance(b, bool) for b in boundary_condition): raise ValueError( From 91fde16f0f73a8376f5d152b49ad2eba8ce40eed Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:48:36 -0400 Subject: [PATCH 15/38] Update pennylane/spin/spin_hamiltonian.py Co-authored-by: Utkarsh --- pennylane/spin/spin_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index fd6ae01f14a..f02de0bc6f0 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -37,7 +37,7 @@ def transverse_ising( Args: lattice (str): Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, ``'Triangle'``, or ``'Kagome'``. n_cells (list[int]): Number of cells in each direction of the grid. - coupling (List[float] or List[math.array[float]]): Coupling between spins, it can be a list of length equal to neighbour_order or a 2D array of shape number of spins * number of spins. Default value is [1.0]. + coupling (List[float] or List[math.array[float]]): Coupling between spins, it can be a list of length equal to ``neighbour_order`` or a square matrix of size ``(num_spins, num_spins)``. Default value is [1.0]. h (float): Value of external magnetic field. Default is 1.0. boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, each element represents the axis for lattice. It defaults to False. neighbour_order (int): Range of neighbours a spin interacts with. Default is 1. From 196fca2aebf1e07025d9423f3f0ae6489bf97d85 Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:49:03 -0400 Subject: [PATCH 16/38] Update pennylane/spin/spin_hamiltonian.py Co-authored-by: Utkarsh --- pennylane/spin/spin_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index f02de0bc6f0..9d7ae471d49 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -50,7 +50,7 @@ def transverse_ising( >>> n_cells = [2,2] >>> J = 0.5 >>> h = 0.1 - >>> spin_ham = transverse_ising(lattice, n_cells, coupling=J, h=h) + >>> spin_ham = transverse_ising("Square", n_cells, coupling=J, h=h) >>> print(spin_ham) -0.5 * (Z(0) @ Z(1)) + -0.5 * (Z(0) @ Z(2)) From 104a3b8cece8ae63c1027297b9c800d385f2036e Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:49:18 -0400 Subject: [PATCH 17/38] Update pennylane/spin/spin_hamiltonian.py Co-authored-by: Utkarsh --- pennylane/spin/spin_hamiltonian.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index 9d7ae471d49..af075f4742e 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -46,7 +46,6 @@ def transverse_ising( pennylane.LinearCombination: Hamiltonian for the transverse-field ising model. **Example** - >>> lattice = "Square" >>> n_cells = [2,2] >>> J = 0.5 >>> h = 0.1 From d0541bfa2f7de90f5139ab9b96849fdb66129933 Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:49:37 -0400 Subject: [PATCH 18/38] Update pennylane/spin/lattice.py Co-authored-by: Utkarsh --- pennylane/spin/lattice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 07186636d69..b8470023db4 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -189,6 +189,7 @@ def add_edge(self, edge_indices): Args: edge_indices: List of edges to be added. + Returns: Updates the edges attribute to include provided edges. """ From a9d61775a6ccf840cebb01c46b796f71d1d9e6fd Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:49:58 -0400 Subject: [PATCH 19/38] Update pennylane/spin/lattice.py Co-authored-by: Utkarsh --- pennylane/spin/lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index b8470023db4..8baf761bae4 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -194,7 +194,7 @@ def add_edge(self, edge_indices): Updates the edges attribute to include provided edges. """ - edges_nocolor = [(v1, v2) for (v1, v2, color) in self.edges] + edges_nocolor = [(v1, v2) for (v1, v2, _) in self.edges] for edge_index in edge_indices: edge_index = tuple(edge_index) if len(edge_index) > 3 or len(edge_index) < 2: From 532d95f8c592a3aacb8c6f090493383fe4e7413c Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:51:39 -0400 Subject: [PATCH 20/38] Update pennylane/spin/spin_hamiltonian.py Co-authored-by: Utkarsh --- pennylane/spin/spin_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index af075f4742e..b9eb8ae450b 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -31,7 +31,7 @@ def transverse_ising( \hat{H} = -J \sum_{} \sigma_i^{z} \sigma_j^{z} - h\sum{i} \sigma_{i}^{x} - where J is the coupling defined for the Hamiltonian, h is the strength of transverse + where ``J`` is the coupling defined for the Hamiltonian, ``h`` is the strength of the transverse magnetic field and i,j represent the indices for neighbouring spins. Args: From 768d7cd2bf02aede23c98600796a19d2f9929f87 Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:53:00 -0400 Subject: [PATCH 21/38] Update pennylane/spin/spin_hamiltonian.py Co-authored-by: Utkarsh --- pennylane/spin/spin_hamiltonian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index b9eb8ae450b..a5e9cacc4ef 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -32,7 +32,7 @@ def transverse_ising( \hat{H} = -J \sum_{} \sigma_i^{z} \sigma_j^{z} - h\sum{i} \sigma_{i}^{x} where ``J`` is the coupling defined for the Hamiltonian, ``h`` is the strength of the transverse - magnetic field and i,j represent the indices for neighbouring spins. + magnetic field and ``i,j`` represent the indices for neighbouring spins. Args: lattice (str): Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, ``'Triangle'``, or ``'Kagome'``. From a18af1a1b4457896ea43d0768308d39b4506bfcf Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Wed, 21 Aug 2024 04:07:05 -0400 Subject: [PATCH 22/38] Addressed reviews --- pennylane/spin/lattice.py | 42 ++++++++++++++++------------- pennylane/spin/spin_hamiltonian.py | 12 ++++++--- tests/spin/test_lattice.py | 29 +++++++++++++------- tests/spin/test_spin_hamiltonian.py | 2 +- 4 files changed, 51 insertions(+), 34 deletions(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 8baf761bae4..0c513ee5b2d 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -18,7 +18,6 @@ """ import itertools -import numpy as np from scipy.spatial import KDTree from pennylane import math @@ -35,9 +34,17 @@ class Lattice: vectors (list[list[float]]): Primitive vectors for the lattice. positions (list[list[float]]): Initial positions of spins. Default value is [[0.0]*number of dimensions]. boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, each element represents the axis for lattice. It defaults to False. - neighbour_order (int): Range of neighbours a spin interacts with. Default is 1. + neighbour_order (int): Specifies the interaction level for neighbors within the lattice. Default is 1 (nearest neighbour). distance_tol (float): Distance below which spatial points are considered equal for the purpose of identifying nearest neighbours, default value is 1e-5. + Raises: + TypeError: + if ``n_cells`` contains numbers other than positive integers. + ValueError: + if ``positions`` doesn't have a dimension of 2. + if ``vectors`` doesn't have a dimension of 2 or the length of vectors is not equal to the number of vectors. + if ``boundary_condition`` is not a bool or a list of bools with length equal to the number of vectors + Returns: Lattice object @@ -62,14 +69,13 @@ def __init__( distance_tol=1e-5, ): - for l in math.asarray(n_cells): - if (not isinstance(l, np.int64)) or l <= 0: - raise TypeError("Argument `n_cells` must be a list of positive integers") + if not all(isinstance(l, int) for l in n_cells) or any(l <= 0 for l in n_cells): + raise TypeError("Argument `n_cells` must be a list of positive integers") vectors = math.asarray(vectors) if vectors.ndim != 2: - raise ValueError("'vectors' must have ndim==2, as array of primitive vectors.") + raise ValueError(f"The dimensions of vectors array must be 2, got {vectors.ndim}.") if vectors.shape[0] != vectors.shape[1]: raise ValueError("The number of primitive vectors must match their length") @@ -79,19 +85,16 @@ def __init__( positions = math.asarray(positions) if positions.ndim != 2: - raise ValueError("'positions' must have ndim==2, as array of initial coordinates.") + raise ValueError(f"The dimensions of positions array must be 2, got {positions.ndim}.") if isinstance(boundary_condition, bool): boundary_condition = [boundary_condition] * len(n_cells) - if not all(isinstance(b, bool) for b in boundary_condition): + if not all(isinstance(b, bool) for b in boundary_condition) or len( + boundary_condition + ) != len(n_cells): raise ValueError( - "Argument 'boundary_condition' must be a bool or a list of bools of same dimensions as the vectors" - ) - - if len(boundary_condition) != len(n_cells): - raise ValueError( - "Argument 'boundary_condition' must be a bool or a list of bools of same dimensions as the vectors" + "Argument 'boundary_condition' must be a bool or a list of bools with length equal to number of vectors" ) self.n_cells = n_cells @@ -107,6 +110,7 @@ def __init__( cutoff = neighbour_order * math.linalg.norm(self.vectors, axis=1).max() + distance_tol edges = self._identify_neighbours(cutoff) self._generate_true_edges(edges, lattice_map, neighbour_order) + self.edges_indices = [(v1, v2) for (v1, v2, color) in self.edges] def _identify_neighbours(self, cutoff): r"""Identifies the connections between lattice points and returns the unique connections @@ -152,7 +156,8 @@ def _generate_grid(self, neighbour_order): """Generates the coordinates of all lattice sites and their indices. Args: - neighbour_order: The number of nearest neighbour interactions. + neighbour_order (int): Specifies the interaction level for neighbors within the lattice. + Returns: lattice_points: The coordinates of all lattice sites. lattice_map: A list to represent the node number for each lattice_point @@ -194,14 +199,13 @@ def add_edge(self, edge_indices): Updates the edges attribute to include provided edges. """ - edges_nocolor = [(v1, v2) for (v1, v2, _) in self.edges] for edge_index in edge_indices: edge_index = tuple(edge_index) if len(edge_index) > 3 or len(edge_index) < 2: - raise ValueError("Edge length can only be 2 or 3.") + raise TypeError("Length of the tuple representing each edge can only be 2 or 3.") if len(edge_index) == 2: - if edge_index in edges_nocolor: + if edge_index in self.edges_indices: raise ValueError("Edge is already present") new_edge = (*edge_index, 0) else: @@ -316,7 +320,7 @@ def _generate_lattice(lattice, n_cells, boundary_condition=False, neighbour_orde lattice (str): Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, ``'Triangle'``, or ``'Kagome'``. n_cells (list[int]): Number of cells in each direction of the grid. boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, each element represents the axis for lattice. It defaults to False. - neighbour_order (int): Range of neighbours a spin interacts with. Default is 1. + neighbour_order (int): Specifies the interaction level for neighbors within the lattice. Default is 1 (nearest neighbour). Returns: lattice object diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index a5e9cacc4ef..bd2f9a29bf9 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -35,12 +35,16 @@ def transverse_ising( magnetic field and ``i,j`` represent the indices for neighbouring spins. Args: - lattice (str): Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, ``'Triangle'``, or ``'Kagome'``. + lattice (str): Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, + ``'Rectangle'``, ``'Honeycomb'``, ``'Triangle'``, or ``'Kagome'``. n_cells (list[int]): Number of cells in each direction of the grid. - coupling (List[float] or List[math.array[float]]): Coupling between spins, it can be a list of length equal to ``neighbour_order`` or a square matrix of size ``(num_spins, num_spins)``. Default value is [1.0]. + coupling (List[float] or List[math.array[float]]): Coupling between spins, it can be a + list of length equal to ``neighbour_order`` or a square matrix of size + ``(num_spins, num_spins)``. Default value is [1.0]. h (float): Value of external magnetic field. Default is 1.0. - boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, each element represents the axis for lattice. It defaults to False. - neighbour_order (int): Range of neighbours a spin interacts with. Default is 1. + boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, + each element represents the axis for lattice. It defaults to False. + neighbour_order (int): Range of neighbours a spin interacts with. Default is 1. Returns: pennylane.LinearCombination: Hamiltonian for the transverse-field ising model. diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index a0b3788c018..e56aa375146 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -16,7 +16,7 @@ """ import pytest -from pennylane import numpy as np +import numpy as np from pennylane.spin import Lattice from pennylane.spin.lattice import _generate_lattice @@ -43,9 +43,7 @@ def test_vectors_error(): r"""Test that an error is raised if a wrong dimension is entered for vectors.""" vectors = [0, 1] n_cells = [2, 2] - with pytest.raises( - ValueError, match="'vectors' must have ndim==2, as array of primitive vectors." - ): + with pytest.raises(ValueError, match="The dimensions of vectors array must be 2, got 1"): Lattice(n_cells=n_cells, vectors=vectors) @@ -54,9 +52,7 @@ def test_positions_error(): vectors = [[0, 1], [1, 0]] n_cells = [2, 2] positions = [0, 0] - with pytest.raises( - ValueError, match="'positions' must have ndim==2, as array of initial coordinates." - ): + with pytest.raises(ValueError, match="The dimensions of positions array must be 2, got 1."): Lattice(n_cells=n_cells, vectors=vectors, positions=positions) @@ -380,10 +376,10 @@ def test_attributes(vectors, positions, n_cells, boundary_condition, n_dim, expe n_cells=n_cells, vectors=vectors, positions=positions, boundary_condition=boundary_condition ) - assert np.all(lattice.vectors == vectors) - assert np.all(lattice.positions == positions) + assert np.allclose(lattice.vectors, vectors) + assert np.allclose(lattice.positions, positions) assert lattice.n_dim == n_dim - assert np.all(lattice.boundary_condition == expected_bc) + assert np.allclose(lattice.boundary_condition, expected_bc) def test_add_edge_error(): @@ -397,6 +393,19 @@ def test_add_edge_error(): lattice.add_edge(edge_indices) +def test_add_edge_error_wrong_type(): + r"""Test that an error is raised if the tuple representing the edge if of wrong length""" + edge_indices = [[4, 5, 1, 0]] + vectors = [[0, 1], [1, 0]] + n_cells = [3, 3] + lattice = Lattice(n_cells=n_cells, vectors=vectors) + + with pytest.raises( + TypeError, match="Length of the tuple representing each edge can only be 2 or 3." + ): + lattice.add_edge(edge_indices) + + def test_add_edge(): r"""Test that edges are added per their index to a lattice""" edge_indices = [[1, 3], [4, 6]] diff --git a/tests/spin/test_spin_hamiltonian.py b/tests/spin/test_spin_hamiltonian.py index 0062d238bcf..4cde37378ea 100644 --- a/tests/spin/test_spin_hamiltonian.py +++ b/tests/spin/test_spin_hamiltonian.py @@ -149,4 +149,4 @@ def test_ising_hamiltonian(shape, n_cells, h, expected_ham): ising_ham = transverse_ising(lattice=shape, n_cells=n_cells, coupling=J, h=h, neighbour_order=1) - assert qml.equal(ising_ham, expected_ham) + qml.assert_equal(ising_ham, expected_ham) From e3aca618e26090c907aaa4aee4fb0d0dc22e1897 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Wed, 21 Aug 2024 04:20:44 -0400 Subject: [PATCH 23/38] vectorized some code --- pennylane/spin/lattice.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 0c513ee5b2d..131a78c298f 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -171,19 +171,13 @@ def _generate_grid(self, neighbour_order): ranges_dim = [range(-wrap_grid[i], Lx + wrap_grid[i]) for i, Lx in enumerate(self.n_cells)] ranges_dim.append(range(n_sl)) + nsites_axis = math.cumprod([n_sl, *self.n_cells[:0:-1]])[::-1] lattice_points = [] lattice_map = [] for Lx in itertools.product(*ranges_dim): point = math.dot(Lx[:-1], self.vectors) + self.positions[Lx[-1]] - node_index = 0 - for i in range(self.n_dim): - node_index += ( - (Lx[i] % self.n_cells[i]) - * math.prod(self.n_cells[self.n_dim - 1 - i : 0 : -1]) - * n_sl - ) - node_index += Lx[-1] + node_index = math.dot(math.mod(Lx[:-1], self.n_cells), nsites_axis) + Lx[-1] lattice_points.append(point) lattice_map.append(node_index) From ac0e9602bc5f537ee06809991e171b06d78d826d Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Wed, 21 Aug 2024 04:22:17 -0400 Subject: [PATCH 24/38] vectorized some code --- tests/spin/test_lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index e56aa375146..f503fdfb766 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -14,9 +14,9 @@ """ Unit tests for functions needed for computing the lattice. """ +import numpy as np import pytest -import numpy as np from pennylane.spin import Lattice from pennylane.spin.lattice import _generate_lattice From c7260a0b7afc0092e5a7042f4d49c0b7470a9f54 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Wed, 21 Aug 2024 06:29:32 -0400 Subject: [PATCH 25/38] Increased test coverage --- pennylane/spin/lattice.py | 7 +-- tests/spin/test_lattice.py | 12 +++++ tests/spin/test_spin_hamiltonian.py | 73 ++++++++++++++++++++++++----- 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 131a78c298f..4e35775696d 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -97,7 +97,7 @@ def __init__( "Argument 'boundary_condition' must be a bool or a list of bools with length equal to number of vectors" ) - self.n_cells = n_cells + self.n_cells = math.asarray(n_cells) self.n_dim = len(n_cells) self.vectors = math.asarray(vectors) self.positions = math.asarray(positions) @@ -164,10 +164,7 @@ def _generate_grid(self, neighbour_order): """ n_sl = len(self.positions) - if self.boundary_condition: - wrap_grid = math.where(self.boundary_condition, neighbour_order, 0) - else: - wrap_grid = math.zeros(self.n_cells.size, dtype=int) + wrap_grid = math.where(self.boundary_condition, neighbour_order, 0) ranges_dim = [range(-wrap_grid[i], Lx + wrap_grid[i]) for i, Lx in enumerate(self.n_cells)] ranges_dim.append(range(n_sl)) diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index f503fdfb766..b74ec3ecddf 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -392,6 +392,10 @@ def test_add_edge_error(): with pytest.raises(ValueError, match="Edge is already present"): lattice.add_edge(edge_indices) + edge_indices = [[4, 5, 0]] + with pytest.raises(ValueError, match="Edge is already present"): + lattice.add_edge(edge_indices) + def test_add_edge_error_wrong_type(): r"""Test that an error is raised if the tuple representing the edge if of wrong length""" @@ -525,3 +529,11 @@ def test_edges_for_shapes(shape, n_cells, expected_edges): r"""Test that correct edges are obtained for given lattice shapes""" lattice = _generate_lattice(lattice=shape, n_cells=n_cells) assert sorted(lattice.edges) == sorted(expected_edges) + + +def test_shape_error(): + r"""Test that an error is raised if wrong shape is provided.""" + n_cells = [5, 5, 5] + lattice = "Octagon" + with pytest.raises(ValueError, match="Lattice shape, 'Octagon' is not supported."): + _generate_lattice(lattice=lattice, n_cells=n_cells) diff --git a/tests/spin/test_spin_hamiltonian.py b/tests/spin/test_spin_hamiltonian.py index 4cde37378ea..4cb8c54d811 100644 --- a/tests/spin/test_spin_hamiltonian.py +++ b/tests/spin/test_spin_hamiltonian.py @@ -23,28 +23,35 @@ def test_coupling_error(): - r"""Test that an error is raised when the provided coupling shape is wrong""" + r"""Test that an error is raised when the provided coupling shape is wrong for + transverse_ising Hamiltonian.""" n_cells = [4, 4] lattice = "Square" with pytest.raises(ValueError, match="Coupling shape should be equal to 1 or 16x16"): transverse_ising(lattice=lattice, n_cells=n_cells, coupling=1.0, neighbour_order=1) -def test_shape_error(): - r"""Test that an error is raised if wrong shape is provided""" - n_cells = [5, 5, 5] - lattice = "Octagon" - with pytest.raises(ValueError, match="Lattice shape, 'Octagon' is not supported."): - transverse_ising(lattice=lattice, n_cells=n_cells, coupling=1.0) - - @pytest.mark.parametrize( # expected_ham here was obtained from datasets - ("shape", "n_cells", "h", "expected_ham"), + ("shape", "n_cells", "J", "h", "expected_ham"), [ ( "chain", [4, 0, 0], + None, + 0, + -1.0 * (Z(0) @ Z(1)) + + -1.0 * (Z(1) @ Z(2)) + + -1.0 * (Z(2) @ Z(3)) + + 0.0 * X(0) + + 0.0 * X(1) + + 0.0 * X(2) + + 0.0 * X(3), + ), + ( + "chain", + [4, 0, 0], + [1.0], 0, -1.0 * (Z(0) @ Z(1)) + -1.0 * (Z(1) @ Z(2)) @@ -57,6 +64,7 @@ def test_shape_error(): ( "chain", [8, 0, 0], + [1.0], -0.17676768, -1.0 * (Z(0) @ Z(1)) + -1.0 * (Z(1) @ Z(2)) @@ -77,6 +85,7 @@ def test_shape_error(): ( "rectangle", [4, 2, 0], + [1.0], -0.25252525, -1.0 * (Z(0) @ Z(1)) + -1.0 * (Z(0) @ Z(2)) @@ -100,6 +109,7 @@ def test_shape_error(): ( "rectangle", [8, 2, 0], + [1.0], -0.44444444, -1.0 * (Z(0) @ Z(1)) + -1.0 * (Z(0) @ Z(2)) @@ -142,10 +152,49 @@ def test_shape_error(): ), ], ) -def test_ising_hamiltonian(shape, n_cells, h, expected_ham): +def test_ising_hamiltonian(shape, n_cells, J, h, expected_ham): r"""Test that the correct Hamiltonian is generated compared to the datasets""" - J = [1.0] + ising_ham = transverse_ising(lattice=shape, n_cells=n_cells, coupling=J, h=h, neighbour_order=1) + + qml.assert_equal(ising_ham, expected_ham) + + +@pytest.mark.parametrize( + # expected_ham here was obtained manually. + ("shape", "n_cells", "J", "h", "expected_ham"), + [ + ( + "chain", + [4, 0, 0], + [[0, 1, 0, 0], [1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 1, 0]], + 0, + -1.0 * (Z(0) @ Z(1)) + + -1.0 * (Z(1) @ Z(2)) + + -1.0 * (Z(2) @ Z(3)) + + 0.0 * X(0) + + 0.0 * X(1) + + 0.0 * X(2) + + 0.0 * X(3), + ), + ( + "square", + [2, 2, 0], + [[0, 0.5, 0.5, 0], [0.5, 0, 0, 0.5], [0.5, 0, 0, 0.5], [0, 0.5, 0.5, 0]], + -1.0, + -0.5 * (Z(0) @ Z(1)) + + -0.5 * (Z(0) @ Z(2)) + + -0.5 * (Z(2) @ Z(3)) + + -0.5 * (Z(1) @ Z(3)) + + 1.0 * X(0) + + 1.0 * X(1) + + 1.0 * X(2) + + 1.0 * X(3), + ), + ], +) +def test_ising_hamiltonian_matrix(shape, n_cells, J, h, expected_ham): + r"""Test that the correct Hamiltonian is generated when coupling is provided as a matrix""" ising_ham = transverse_ising(lattice=shape, n_cells=n_cells, coupling=J, h=h, neighbour_order=1) From 6439a47490cef864bcda6f6f13c0fd8e23565be5 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Wed, 21 Aug 2024 07:33:38 -0400 Subject: [PATCH 26/38] Increased test coverage --- tests/spin/test_lattice.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index b74ec3ecddf..ce71ce9fde1 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -398,7 +398,7 @@ def test_add_edge_error(): def test_add_edge_error_wrong_type(): - r"""Test that an error is raised if the tuple representing the edge if of wrong length""" + r"""Test that an error is raised if the tuple representing the edge is of wrong length""" edge_indices = [[4, 5, 1, 0]] vectors = [[0, 1], [1, 0]] n_cells = [3, 3] @@ -416,9 +416,11 @@ def test_add_edge(): vectors = [[0, 1], [1, 0]] n_cells = [3, 3] lattice = Lattice(n_cells=n_cells, vectors=vectors) - lattice.add_edge(edge_indices) + lattice.add_edge([[0, 2, 1]]) assert np.all(np.isin(edge_indices, lattice.edges)) + print(lattice.edges) + assert np.all(np.isin([0, 2, 1], lattice.edges)) @pytest.mark.parametrize( From a81fe95939cd2c5e10b554c6536d7689ec608fbd Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Wed, 21 Aug 2024 08:40:20 -0400 Subject: [PATCH 27/38] Addressed review comments --- pennylane/spin/lattice.py | 3 ++- pennylane/spin/spin_hamiltonian.py | 6 ++++-- tests/spin/test_lattice.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 4e35775696d..328990bb881 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -114,7 +114,8 @@ def __init__( def _identify_neighbours(self, cutoff): r"""Identifies the connections between lattice points and returns the unique connections - based on the neighbour_order""" + based on the neighbour_order. This function uses KDTree to identify neighbours, which follows + depth first search traversal.""" tree = KDTree(self.lattice_points) indices = tree.query_ball_tree(tree, cutoff) diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index bd2f9a29bf9..29f0ad9ec28 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -50,6 +50,7 @@ def transverse_ising( pennylane.LinearCombination: Hamiltonian for the transverse-field ising model. **Example** + >>> n_cells = [2,2] >>> J = 0.5 >>> h = 0.1 @@ -69,11 +70,12 @@ def transverse_ising( if coupling is None: coupling = [1.0] coupling = math.asarray(coupling) + hamiltonian = 0.0 - print(coupling.shape) + if coupling.shape not in [(neighbour_order,), (lattice.n_sites, lattice.n_sites)]: raise ValueError( - f"Coupling shape should be equal to {neighbour_order} or {lattice.n_sites}x{lattice.n_sites}" + f"Coupling shape should be equal to {neighbour_order}x1 or {lattice.n_sites}x{lattice.n_sites}" ) if coupling.shape == (neighbour_order,): diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index ce71ce9fde1..2c8f9887196 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -253,6 +253,22 @@ def test_lattice_points(vectors, positions, n_cells, expected_number): (4, 7, 0), ], ), + ( + [[1, 0], [0.5, np.sqrt(3) / 2]], + [[0.5, 0.5 / 3**0.5], [1, 1 / 3**0.5]], + [2, 2], + False, + [ + (0, 1, 0), + (1, 2, 0), + (1, 4, 0), + (2, 3, 0), + (3, 6, 0), + (4, 5, 0), + (5, 6, 0), + (6, 7, 0), + ], + ), ], ) def test_boundary_condition(vectors, positions, n_cells, boundary_condition, expected_edges): From 6b388a41ae4ad3b609e72a141ee407b486e31f0e Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:30:09 -0400 Subject: [PATCH 28/38] Update pennylane/spin/lattice.py Co-authored-by: Utkarsh --- pennylane/spin/lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 328990bb881..0d25bd7353d 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -138,7 +138,7 @@ def _identify_neighbours(self, cutoff): edges[scaled_dist] = [] edges[scaled_dist].append((i, neighbour)) - edges = [value for key, value in sorted(edges.items())] + edges = [value for _, value in sorted(edges.items())] return edges def _generate_true_edges(self, edges, map, neighbour_order): From 9b98c45e3ec6701ce59faa19c8e528d3693877ec Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:30:17 -0400 Subject: [PATCH 29/38] Update pennylane/spin/lattice.py Co-authored-by: Utkarsh --- pennylane/spin/lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 0d25bd7353d..03fb0636e7d 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -107,7 +107,7 @@ def __init__( self.n_sites = math.prod(n_cells) * n_sl self.lattice_points, lattice_map = self._generate_grid(neighbour_order) - cutoff = neighbour_order * math.linalg.norm(self.vectors, axis=1).max() + distance_tol + cutoff = neighbour_order * math.max(math.linalg.norm(self.vectors, axis=1)) + distance_tol edges = self._identify_neighbours(cutoff) self._generate_true_edges(edges, lattice_map, neighbour_order) self.edges_indices = [(v1, v2) for (v1, v2, color) in self.edges] From 3849ae03e045c61cf158aa02a0d18977c515985a Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:30:54 -0400 Subject: [PATCH 30/38] Update pennylane/spin/spin_hamiltonian.py Co-authored-by: Utkarsh --- pennylane/spin/spin_hamiltonian.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index 29f0ad9ec28..5b2f9845968 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -26,7 +26,9 @@ def transverse_ising( lattice, n_cells, coupling=None, h=1.0, boundary_condition=False, neighbour_order=1 ): r"""Generates the transverse field Ising model on a lattice. + The Hamiltonian is represented as: + .. math:: \hat{H} = -J \sum_{} \sigma_i^{z} \sigma_j^{z} - h\sum{i} \sigma_{i}^{x} From 856acd0e155795ddf15156ee09dc6ea29f459b62 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Wed, 21 Aug 2024 12:34:18 -0400 Subject: [PATCH 31/38] [skip ci] addressed comments --- pennylane/spin/lattice.py | 24 +++++++++++++----------- pennylane/spin/spin_hamiltonian.py | 2 +- tests/spin/test_lattice.py | 26 +++++--------------------- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 03fb0636e7d..7450e4bf984 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -72,20 +72,22 @@ def __init__( if not all(isinstance(l, int) for l in n_cells) or any(l <= 0 for l in n_cells): raise TypeError("Argument `n_cells` must be a list of positive integers") - vectors = math.asarray(vectors) + self.vectors = math.asarray(vectors) - if vectors.ndim != 2: - raise ValueError(f"The dimensions of vectors array must be 2, got {vectors.ndim}.") + if self.vectors.ndim != 2: + raise ValueError(f"The dimensions of vectors array must be 2, got {self.vectors.ndim}.") - if vectors.shape[0] != vectors.shape[1]: + if self.vectors.shape[0] != self.vectors.shape[1]: raise ValueError("The number of primitive vectors must match their length") if positions is None: - positions = math.zeros(vectors.shape[0])[None, :] - positions = math.asarray(positions) + positions = math.zeros(self.vectors.shape[0])[None, :] + self.positions = math.asarray(positions) - if positions.ndim != 2: - raise ValueError(f"The dimensions of positions array must be 2, got {positions.ndim}.") + if self.positions.ndim != 2: + raise ValueError( + f"The dimensions of positions array must be 2, got {self.positions.ndim}." + ) if isinstance(boundary_condition, bool): boundary_condition = [boundary_condition] * len(n_cells) @@ -99,8 +101,6 @@ def __init__( self.n_cells = math.asarray(n_cells) self.n_dim = len(n_cells) - self.vectors = math.asarray(vectors) - self.positions = math.asarray(positions) self.boundary_condition = boundary_condition n_sl = len(self.positions) @@ -120,7 +120,8 @@ def _identify_neighbours(self, cutoff): tree = KDTree(self.lattice_points) indices = tree.query_ball_tree(tree, cutoff) # Number to Scale the distance - bin_density = 21621600 # multiple of expected denominators + # Scales the distance to sort edges into appropriate bins + bin_density = 2 ^ 5 * 3 ^ 3 * 5 ^ 2 * 7 * 11 * 13 # multiple of expected denominators unique_pairs = set() edges = {} for i, neighbours in enumerate(indices): @@ -305,6 +306,7 @@ def _kagome(n_cells, boundary_condition=False, neighbour_order=1): return lattice_kagome +## Todo: Check the efficiency of this function with a dictionary instead. def _generate_lattice(lattice, n_cells, boundary_condition=False, neighbour_order=1): r"""Generates the lattice object for given shape and n_cells. diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index 5b2f9845968..f394f4bd9f8 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -26,7 +26,7 @@ def transverse_ising( lattice, n_cells, coupling=None, h=1.0, boundary_condition=False, neighbour_order=1 ): r"""Generates the transverse field Ising model on a lattice. - + The Hamiltonian is represented as: .. math:: diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index 2c8f9887196..d1c13800d5d 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -23,20 +23,13 @@ # pylint: disable=too-many-arguments, too-many-instance-attributes -def test_boundary_condition_dimension_error(): - r"""Test that an error is raised if a wrong dimensions are entered for boundary_condition.""" - vectors = [[1]] - n_cells = [10] - with pytest.raises(ValueError, match="Argument 'boundary_condition' must be a bool"): - Lattice(n_cells=n_cells, vectors=vectors, boundary_condition=[True, True]) - - -def test_boundary_condition_type_error(): +@pytest.mark.parametrize(("boundary_condition"), [([True, True]), ([4])]) +def test_boundary_condition_type_error(boundary_condition): r"""Test that an error is raised if a wrong type is entered for boundary_condition.""" vectors = [[1]] n_cells = [10] with pytest.raises(ValueError, match="Argument 'boundary_condition' must be a bool"): - Lattice(n_cells=n_cells, vectors=vectors, boundary_condition=[4]) + Lattice(n_cells=n_cells, vectors=vectors, boundary_condition=boundary_condition) def test_vectors_error(): @@ -64,20 +57,11 @@ def test_vectors_shape_error(): Lattice(n_cells=n_cells, vectors=vectors) -def test_n_cells_error(): - r"""Test that an error is raised if length of vectors is provided in negative.""" - - vectors = [[0, 1], [1, 0]] - n_cells = [2, -2] - with pytest.raises(TypeError, match="Argument `n_cells` must be a list of positive integers"): - Lattice(n_cells=n_cells, vectors=vectors) - - -def test_n_cells_type_error(): +@pytest.mark.parametrize(("n_cells"), [([2, -2]), ([2, 2.4])]) +def test_n_cells_type_error(n_cells): r"""Test that an error is raised if length of vectors is provided not as an int.""" vectors = [[0, 1], [1, 0]] - n_cells = [2, 2.4] with pytest.raises(TypeError, match="Argument `n_cells` must be a list of positive integers"): Lattice(n_cells=n_cells, vectors=vectors) From 794d4a11e8060ed8032f5ec67732479623df85b4 Mon Sep 17 00:00:00 2001 From: soranjh Date: Wed, 21 Aug 2024 16:09:57 -0400 Subject: [PATCH 32/38] add minor doc fixes --- doc/releases/changelog-dev.md | 3 +-- pennylane/spin/__init__.py | 2 +- pennylane/spin/lattice.py | 37 +++++++++++++++++++---------------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index ad26d6c169d..f3c1fb67d42 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -26,8 +26,7 @@

Creating spin Hamiltonians 🧑‍🎨

-* New function to generate transverse-field Ising Hamiltonian and the required helper functions to generate the lattice - have been added. +* The function ``transverse_ising`` is added to generate transverse-field Ising Hamiltonian. [(#6106)](https://github.com/PennyLaneAI/pennylane/pull/6106)

Improvements 🛠

diff --git a/pennylane/spin/__init__.py b/pennylane/spin/__init__.py index d24bda7b2df..47b50a1e60f 100644 --- a/pennylane/spin/__init__.py +++ b/pennylane/spin/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module provides the functionality to create Lattices and spin Hamiltonians. +This module provides the functionality to create spin Hamiltonians. """ from .lattice import Lattice diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 7450e4bf984..7a63bd61f16 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -13,7 +13,7 @@ # limitations under the License. """ This file contains functions and classes to create a -:class:`~pennylane.spin.lattice` object. This object stores all +:class:`~pennylane.spin.Lattice` object. This object stores all the necessary information about a lattice. """ import itertools @@ -32,10 +32,14 @@ class Lattice: Args: n_cells (list[int]): Number of cells in each direction of the grid. vectors (list[list[float]]): Primitive vectors for the lattice. - positions (list[list[float]]): Initial positions of spins. Default value is [[0.0]*number of dimensions]. - boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, each element represents the axis for lattice. It defaults to False. - neighbour_order (int): Specifies the interaction level for neighbors within the lattice. Default is 1 (nearest neighbour). - distance_tol (float): Distance below which spatial points are considered equal for the purpose of identifying nearest neighbours, default value is 1e-5. + positions (list[list[float]]): Initial positions of spin cites. Default value is + ``[[0.0]*number of dimensions]``. + boundary_condition (bool or list[bool]): Defines boundary conditions different lattice axes, + default is ``False`` indicating open boundary condition. + neighbour_order (int): Specifies the interaction level for neighbors within the lattice. + Default is 1 (nearest neighbour). + distance_tol (float): Distance below which spatial points are considered equal for the + purpose of identifying nearest neighbours, default value is 1e-5. Raises: TypeError: @@ -52,11 +56,10 @@ class Lattice: >>> n_cells = [2,2] >>> vectors = [[0, 1], [1, 0]] >>> boundary_condition = [True, False] - >>> lattice = Lattice(n_cells, vectors, - >>> boundary_condition=boundary_condition) + >>> lattice = qml.spin.Lattice(n_cells, vectors, + >>> boundary_condition=boundary_condition) >>> print(lattice.edges) - [(3, 4, 0), (0, 3, 0), (4, 5, 0), (1, 4, 0), - (2, 5, 0), (0, 1, 0), (1, 2, 0)] + [(2, 3, 0), (0, 2, 0), (1, 3, 0), (0, 1, 0)] """ def __init__( @@ -114,14 +117,14 @@ def __init__( def _identify_neighbours(self, cutoff): r"""Identifies the connections between lattice points and returns the unique connections - based on the neighbour_order. This function uses KDTree to identify neighbours, which follows - depth first search traversal.""" + based on the neighbour_order. This function uses KDTree to identify neighbours, which + follows depth first search traversal.""" tree = KDTree(self.lattice_points) indices = tree.query_ball_tree(tree, cutoff) - # Number to Scale the distance - # Scales the distance to sort edges into appropriate bins - bin_density = 2 ^ 5 * 3 ^ 3 * 5 ^ 2 * 7 * 11 * 13 # multiple of expected denominators + # Number to scale the distance, needed to sort edges into appropriate bins, it is currently + # set as a multiple of expected denominators. + bin_density = 2 ^ 5 * 3 ^ 3 * 5 ^ 2 * 7 * 11 * 13 unique_pairs = set() edges = {} for i, neighbours in enumerate(indices): @@ -162,7 +165,7 @@ def _generate_grid(self, neighbour_order): Returns: lattice_points: The coordinates of all lattice sites. - lattice_map: A list to represent the node number for each lattice_point + lattice_map: A list to represent the node number for each lattice_point. """ n_sl = len(self.positions) @@ -306,12 +309,12 @@ def _kagome(n_cells, boundary_condition=False, neighbour_order=1): return lattice_kagome -## Todo: Check the efficiency of this function with a dictionary instead. +# TODO Check the efficiency of this function with a dictionary instead. def _generate_lattice(lattice, n_cells, boundary_condition=False, neighbour_order=1): r"""Generates the lattice object for given shape and n_cells. Args: - lattice (str): Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, ``'Rectangle'``, ``'Honeycomb'``, ``'Triangle'``, or ``'Kagome'``. + lattice (str): Shape of the lattice. Input Values can be ``'chain'``, ``'square'``, ``'rectangle'``, ``'honeycomb'``, ``'triangle'``, or ``'kagome'``. n_cells (list[int]): Number of cells in each direction of the grid. boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, each element represents the axis for lattice. It defaults to False. neighbour_order (int): Specifies the interaction level for neighbors within the lattice. Default is 1 (nearest neighbour). From a3f76db40ce4fbc5ba5aa5051e3f00a6a9ce656a Mon Sep 17 00:00:00 2001 From: soranjh Date: Wed, 21 Aug 2024 16:20:03 -0400 Subject: [PATCH 33/38] add minor doc fixes --- pennylane/spin/spin_hamiltonian.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index f394f4bd9f8..1800f422789 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This file contains functions to create different templates of spin Hamiltonians. +This file contains functions to create spin Hamiltonians. """ from pennylane import X, Z, math @@ -25,27 +25,27 @@ def transverse_ising( lattice, n_cells, coupling=None, h=1.0, boundary_condition=False, neighbour_order=1 ): - r"""Generates the transverse field Ising model on a lattice. + r"""Generates the transverse-field Ising model on a lattice. The Hamiltonian is represented as: .. math:: - \hat{H} = -J \sum_{} \sigma_i^{z} \sigma_j^{z} - h\sum{i} \sigma_{i}^{x} + \hat{H} = -J \sum_{} \sigma_i^{z} \sigma_j^{z} - h\sum_{i} \sigma_{i}^{x} - where ``J`` is the coupling defined for the Hamiltonian, ``h`` is the strength of the transverse - magnetic field and ``i,j`` represent the indices for neighbouring spins. + where ``J`` is the coupling parameter defined for the Hamiltonian, ``h`` is the strength of the + transverse magnetic field and ``i,j`` represent the indices for neighbouring spins. Args: - lattice (str): Shape of the lattice. Input Values can be ``'Chain'``, ``'Square'``, - ``'Rectangle'``, ``'Honeycomb'``, ``'Triangle'``, or ``'Kagome'``. + lattice (str): Shape of the lattice. Input Values can be ``'chain'``, ``'square'``, + ``'rectangle'``, ``'honeycomb'``, ``'triangle'``, or ``'kagome'``. n_cells (list[int]): Number of cells in each direction of the grid. coupling (List[float] or List[math.array[float]]): Coupling between spins, it can be a - list of length equal to ``neighbour_order`` or a square matrix of size - ``(num_spins, num_spins)``. Default value is [1.0]. + list of length equal to ``neighbour_order`` or a square matrix of size + ``(num_spins, num_spins)``. Default value is [1.0]. h (float): Value of external magnetic field. Default is 1.0. - boundary_condition (bool or list[bool]): Defines boundary conditions, False for open boundary condition, - each element represents the axis for lattice. It defaults to False. + boundary_condition (bool or list[bool]): Defines boundary conditions different lattice axes, + default is ``False`` indicating open boundary condition. neighbour_order (int): Range of neighbours a spin interacts with. Default is 1. Returns: From 20d1735a3638ddb712575b78b8e4d399e457284f Mon Sep 17 00:00:00 2001 From: soranjh Date: Wed, 21 Aug 2024 16:31:35 -0400 Subject: [PATCH 34/38] add minor doc fixes --- tests/spin/test_lattice.py | 2 +- tests/spin/test_spin_hamiltonian.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index d1c13800d5d..8ddb796edf6 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Unit tests for functions needed for computing the lattice. +Unit tests for functions and classes needed for construct a lattice. """ import numpy as np import pytest diff --git a/tests/spin/test_spin_hamiltonian.py b/tests/spin/test_spin_hamiltonian.py index 4cb8c54d811..30fd2f7f99f 100644 --- a/tests/spin/test_spin_hamiltonian.py +++ b/tests/spin/test_spin_hamiltonian.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Unit tests for functions needed for computing the spin Hamiltonians. +Unit tests for functions needed for computing a spin Hamiltonian. """ import pytest From 683bbcdc6921cbd7dc09cca8435bbd8969802e8d Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Thu, 22 Aug 2024 07:38:55 -0400 Subject: [PATCH 35/38] Fixed tests --- pennylane/spin/lattice.py | 22 +++++++++++++--------- pennylane/spin/spin_hamiltonian.py | 12 ++++++------ tests/spin/test_spin_hamiltonian.py | 6 ++++-- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index 7a63bd61f16..a6c7965a862 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -112,7 +112,7 @@ def __init__( cutoff = neighbour_order * math.max(math.linalg.norm(self.vectors, axis=1)) + distance_tol edges = self._identify_neighbours(cutoff) - self._generate_true_edges(edges, lattice_map, neighbour_order) + self.edges = self._generate_true_edges(edges, lattice_map, neighbour_order) self.edges_indices = [(v1, v2) for (v1, v2, color) in self.edges] def _identify_neighbours(self, cutoff): @@ -148,14 +148,15 @@ def _identify_neighbours(self, cutoff): def _generate_true_edges(self, edges, map, neighbour_order): r"""Modifies the edges to remove hidden nodes and create connections based on boundary_conditions""" - self.edges = [] + true_edges = [] for i, edge in enumerate(edges): if i >= neighbour_order: break for e1, e2 in edge: true_edge = (min(map[e1], map[e2]), max(map[e1], map[e2]), i) - if true_edge not in self.edges: - self.edges.append(true_edge) + if true_edge not in true_edges: + true_edges.append(true_edge) + return true_edges def _generate_grid(self, neighbour_order): """Generates the coordinates of all lattice sites and their indices. @@ -171,15 +172,17 @@ def _generate_grid(self, neighbour_order): n_sl = len(self.positions) wrap_grid = math.where(self.boundary_condition, neighbour_order, 0) - ranges_dim = [range(-wrap_grid[i], Lx + wrap_grid[i]) for i, Lx in enumerate(self.n_cells)] + ranges_dim = [ + range(-wrap_grid[i], cell + wrap_grid[i]) for i, cell in enumerate(self.n_cells) + ] ranges_dim.append(range(n_sl)) nsites_axis = math.cumprod([n_sl, *self.n_cells[:0:-1]])[::-1] lattice_points = [] lattice_map = [] - for Lx in itertools.product(*ranges_dim): - point = math.dot(Lx[:-1], self.vectors) + self.positions[Lx[-1]] - node_index = math.dot(math.mod(Lx[:-1], self.n_cells), nsites_axis) + Lx[-1] + for cell in itertools.product(*ranges_dim): + point = math.dot(cell[:-1], self.vectors) + self.positions[cell[-1]] + node_index = math.dot(math.mod(cell[:-1], self.n_cells), nsites_axis) + cell[-1] lattice_points.append(point) lattice_map.append(node_index) @@ -189,7 +192,8 @@ def add_edge(self, edge_indices): r"""Adds a specific edge based on the site index without translating it. Args: - edge_indices: List of edges to be added. + edge_indices: List of edges to be added, an edge is defined as a list of integers + specifying the corresponding node indices. Returns: Updates the edges attribute to include provided edges. diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index 1800f422789..d3fe985fd67 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -57,27 +57,27 @@ def transverse_ising( >>> J = 0.5 >>> h = 0.1 >>> spin_ham = transverse_ising("Square", n_cells, coupling=J, h=h) - >>> print(spin_ham) + >>> spin_ham -0.5 * (Z(0) @ Z(1)) + -0.5 * (Z(0) @ Z(2)) + -0.5 * (Z(1) @ Z(3)) + -0.5 * (Z(2) @ Z(3)) - + -0.1 * X(0) - + -0.1 * X(1) - + -0.1 * X(2) - + -0.1 * X(3) + + -0.1 * X(0) + -0.1 * X(1) + + -0.1 * X(2) + -0.1 * X(3) """ lattice = _generate_lattice(lattice, n_cells, boundary_condition, neighbour_order) if coupling is None: coupling = [1.0] + elif isinstance(coupling, (int, float, complex)): + coupling = [coupling] coupling = math.asarray(coupling) hamiltonian = 0.0 if coupling.shape not in [(neighbour_order,), (lattice.n_sites, lattice.n_sites)]: raise ValueError( - f"Coupling shape should be equal to {neighbour_order}x1 or {lattice.n_sites}x{lattice.n_sites}" + f"Coupling should be a number or an array of shape {neighbour_order}x1 or {lattice.n_sites}x{lattice.n_sites}" ) if coupling.shape == (neighbour_order,): diff --git a/tests/spin/test_spin_hamiltonian.py b/tests/spin/test_spin_hamiltonian.py index 30fd2f7f99f..be7ad0203e2 100644 --- a/tests/spin/test_spin_hamiltonian.py +++ b/tests/spin/test_spin_hamiltonian.py @@ -27,8 +27,10 @@ def test_coupling_error(): transverse_ising Hamiltonian.""" n_cells = [4, 4] lattice = "Square" - with pytest.raises(ValueError, match="Coupling shape should be equal to 1 or 16x16"): - transverse_ising(lattice=lattice, n_cells=n_cells, coupling=1.0, neighbour_order=1) + with pytest.raises( + ValueError, match="Coupling should be a number or an array of shape 1x1 or 16x16" + ): + transverse_ising(lattice=lattice, n_cells=n_cells, coupling=[1.0, 2.0], neighbour_order=1) @pytest.mark.parametrize( From 260c60c882cca4c5092d726a3dfb532c1daffa04 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Thu, 22 Aug 2024 08:01:45 -0400 Subject: [PATCH 36/38] Fixed CI --- pennylane/spin/lattice.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pennylane/spin/lattice.py b/pennylane/spin/lattice.py index a6c7965a862..9c823d8bc73 100644 --- a/pennylane/spin/lattice.py +++ b/pennylane/spin/lattice.py @@ -112,7 +112,7 @@ def __init__( cutoff = neighbour_order * math.max(math.linalg.norm(self.vectors, axis=1)) + distance_tol edges = self._identify_neighbours(cutoff) - self.edges = self._generate_true_edges(edges, lattice_map, neighbour_order) + self.edges = Lattice._generate_true_edges(edges, lattice_map, neighbour_order) self.edges_indices = [(v1, v2) for (v1, v2, color) in self.edges] def _identify_neighbours(self, cutoff): @@ -145,7 +145,8 @@ def _identify_neighbours(self, cutoff): edges = [value for _, value in sorted(edges.items())] return edges - def _generate_true_edges(self, edges, map, neighbour_order): + @staticmethod + def _generate_true_edges(edges, map, neighbour_order): r"""Modifies the edges to remove hidden nodes and create connections based on boundary_conditions""" true_edges = [] From bf65324fa3d1731fb8f7c115ebd3a3229b7029ee Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Thu, 22 Aug 2024 08:48:14 -0400 Subject: [PATCH 37/38] Added more test coverage --- pennylane/spin/spin_hamiltonian.py | 5 +++-- tests/spin/test_spin_hamiltonian.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pennylane/spin/spin_hamiltonian.py b/pennylane/spin/spin_hamiltonian.py index d3fe985fd67..d5cad340294 100644 --- a/pennylane/spin/spin_hamiltonian.py +++ b/pennylane/spin/spin_hamiltonian.py @@ -40,13 +40,14 @@ def transverse_ising( lattice (str): Shape of the lattice. Input Values can be ``'chain'``, ``'square'``, ``'rectangle'``, ``'honeycomb'``, ``'triangle'``, or ``'kagome'``. n_cells (list[int]): Number of cells in each direction of the grid. - coupling (List[float] or List[math.array[float]]): Coupling between spins, it can be a + coupling (float or List[float] or List[math.array[float]]): Coupling between spins, it can be a list of length equal to ``neighbour_order`` or a square matrix of size ``(num_spins, num_spins)``. Default value is [1.0]. h (float): Value of external magnetic field. Default is 1.0. boundary_condition (bool or list[bool]): Defines boundary conditions different lattice axes, default is ``False`` indicating open boundary condition. - neighbour_order (int): Range of neighbours a spin interacts with. Default is 1. + neighbour_order (int): Specifies the interaction level for neighbors within the lattice. + Default is 1 (nearest neighbour). Returns: pennylane.LinearCombination: Hamiltonian for the transverse-field ising model. diff --git a/tests/spin/test_spin_hamiltonian.py b/tests/spin/test_spin_hamiltonian.py index be7ad0203e2..600b2fdafca 100644 --- a/tests/spin/test_spin_hamiltonian.py +++ b/tests/spin/test_spin_hamiltonian.py @@ -53,7 +53,7 @@ def test_coupling_error(): ( "chain", [4, 0, 0], - [1.0], + 1.0, 0, -1.0 * (Z(0) @ Z(1)) + -1.0 * (Z(1) @ Z(2)) From ad9dc4c05dd65b9f1232b84b493090664ff36418 Mon Sep 17 00:00:00 2001 From: ddhawan11 Date: Thu, 22 Aug 2024 09:40:46 -0400 Subject: [PATCH 38/38] Minor doc fix --- tests/spin/test_lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/spin/test_lattice.py b/tests/spin/test_lattice.py index 8ddb796edf6..6576a396f3d 100644 --- a/tests/spin/test_lattice.py +++ b/tests/spin/test_lattice.py @@ -136,7 +136,7 @@ def test_n_cells_type_error(n_cells): ], ) def test_positions(vectors, positions, n_cells, expected_points): - r"""Test that the lattice points start from the coordinates provided in the positions""" + r"""Test that the lattice points are translated according to coordinates provided in the positions.""" lattice = Lattice(n_cells=n_cells, vectors=vectors, positions=positions) assert np.allclose(expected_points, lattice.lattice_points)