From 353239b1a1adcaa7846c5ab93e3c24fc7779cd92 Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Fri, 30 Jan 2026 13:17:53 -0800 Subject: [PATCH 01/13] Initial commit; hypergraph module added --- .../pip/qsharp/magnets/geometry/__init__.py | 10 ++ .../pip/qsharp/magnets/geometry/hypergraph.py | 108 +++++++++++++ source/pip/tests/magnets/__init__.py | 1 + source/pip/tests/magnets/test_hypergraph.py | 145 ++++++++++++++++++ 4 files changed, 264 insertions(+) create mode 100644 source/pip/qsharp/magnets/geometry/__init__.py create mode 100644 source/pip/qsharp/magnets/geometry/hypergraph.py create mode 100644 source/pip/tests/magnets/__init__.py create mode 100755 source/pip/tests/magnets/test_hypergraph.py diff --git a/source/pip/qsharp/magnets/geometry/__init__.py b/source/pip/qsharp/magnets/geometry/__init__.py new file mode 100644 index 0000000000..53d82f3051 --- /dev/null +++ b/source/pip/qsharp/magnets/geometry/__init__.py @@ -0,0 +1,10 @@ +"""Geometry module for representing quantum system topologies. + +This module provides hypergraph data structures for representing the +geometric structure of quantum systems, including lattice topologies +and interaction graphs. +""" + +from .hypergraph import Hyperedge, Hypergraph + +__all__ = ["Hyperedge", "Hypergraph"] diff --git a/source/pip/qsharp/magnets/geometry/hypergraph.py b/source/pip/qsharp/magnets/geometry/hypergraph.py new file mode 100644 index 0000000000..4486ac0e97 --- /dev/null +++ b/source/pip/qsharp/magnets/geometry/hypergraph.py @@ -0,0 +1,108 @@ +"""Hypergraph data structures for representing quantum system geometries. + +This module provides classes for representing hypergraphs, which generalize +graphs by allowing edges (hyperedges) to connect any number of vertices. +Hypergraphs are useful for representing interaction terms in quantum +Hamiltonians, where multi-body interactions can involve more than two sites. +""" + +from typing import Iterator, List + + +class Hyperedge: + """A hyperedge connecting one or more vertices in a hypergraph. + + A hyperedge generalizes the concept of an edge in a graph. While a + traditional edge connects exactly two vertices, a hyperedge can connect + any number of vertices. This is useful for representing: + - Single-site terms (self-loops): 1 vertex + - Two-body interactions: 2 vertices + - Multi-body interactions: 3+ vertices + + Attributes: + vertices: Sorted list of vertex indices connected by this hyperedge. + + Example: + >>> edge = Hyperedge([2, 0, 1]) + >>> edge.vertices + [0, 1, 2] + """ + + def __init__(self, vertices: List[int]) -> None: + """Initialize a hyperedge with the given vertices. + + Args: + vertices: List of vertex indices. Will be sorted internally. + """ + self.vertices: List[int] = sorted(vertices) + + def __repr__(self) -> str: + return f"Hyperedge({self.vertices})" + + +class Hypergraph: + """A hypergraph consisting of vertices connected by hyperedges. + + A hypergraph is a generalization of a graph where edges (hyperedges) can + connect any number of vertices. This class serves as the base class for + various lattice geometries used in quantum simulations. + + Attributes: + _edges: List of hyperedges in the order they were added. + _vertex_set: Set of all unique vertex indices in the hypergraph. + _edge_list: Set of hyperedges for efficient membership testing. + + Example: + >>> edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([0, 2])] + >>> graph = Hypergraph(edges) + >>> graph.nvertices() + 3 + >>> graph.nedges() + 3 + """ + + def __init__(self, edges: List[Hyperedge]) -> None: + """Initialize a hypergraph with the given edges. + + Args: + edges: List of hyperedges defining the hypergraph structure. + """ + self._edges = edges + self._vertex_set = set() + self._edge_list = set(edges) + for edge in edges: + self._vertex_set.update(edge.vertices) + + def nedges(self) -> int: + """Return the number of hyperedges in the hypergraph.""" + return len(self._edges) + + def nvertices(self) -> int: + """Return the number of vertices in the hypergraph.""" + return len(self._vertex_set) + + def vertices(self) -> Iterator[int]: + """Return an iterator over vertices in sorted order. + + Returns: + Iterator yielding vertex indices in ascending order. + """ + return iter(sorted(self._vertex_set)) + + def edges(self, part: int = 0) -> Iterator[Hyperedge]: + """Return an iterator over hyperedges in the hypergraph. + + Args: + part: Partition index (reserved for subclass implementations + that support edge partitioning for parallel updates). + + Returns: + Iterator over all hyperedges in the hypergraph. + """ + return iter(self._edge_list) + + def __str__(self) -> str: + return f"Hypergraph with {self.nvertices()} vertices and {self.nedges()} edges." + + def __repr__(self) -> str: + return f"Hypergraph({list(self._edges)})" diff --git a/source/pip/tests/magnets/__init__.py b/source/pip/tests/magnets/__init__.py new file mode 100644 index 0000000000..a424bf492f --- /dev/null +++ b/source/pip/tests/magnets/__init__.py @@ -0,0 +1 @@ +"""Unit tests for the magnets library.""" diff --git a/source/pip/tests/magnets/test_hypergraph.py b/source/pip/tests/magnets/test_hypergraph.py new file mode 100755 index 0000000000..9e30dfccbe --- /dev/null +++ b/source/pip/tests/magnets/test_hypergraph.py @@ -0,0 +1,145 @@ +"""Unit tests for hypergraph data structures.""" + +import unittest +from qsharp.magnets.geometry.hypergraph import Hyperedge, Hypergraph + + +class TestHyperedge(unittest.TestCase): + """Test cases for the Hyperedge class.""" + + def test_init_basic(self): + """Test basic Hyperedge initialization.""" + edge = Hyperedge([0, 1]) + self.assertEqual(edge.vertices, [0, 1]) + + def test_vertices_sorted(self): + """Test that vertices are automatically sorted.""" + edge = Hyperedge([3, 1, 2]) + self.assertEqual(edge.vertices, [1, 2, 3]) + + def test_single_vertex(self): + """Test hyperedge with single vertex (self-loop).""" + edge = Hyperedge([5]) + self.assertEqual(edge.vertices, [5]) + self.assertEqual(len(edge.vertices), 1) + + def test_multiple_vertices(self): + """Test hyperedge with multiple vertices (multi-body interaction).""" + edge = Hyperedge([0, 1, 2, 3]) + self.assertEqual(edge.vertices, [0, 1, 2, 3]) + self.assertEqual(len(edge.vertices), 4) + + def test_repr(self): + """Test string representation.""" + edge = Hyperedge([1, 0]) + self.assertEqual(repr(edge), "Hyperedge([0, 1])") + + def test_empty_vertices(self): + """Test hyperedge with empty vertex list.""" + edge = Hyperedge([]) + self.assertEqual(edge.vertices, []) + self.assertEqual(len(edge.vertices), 0) + + +class TestHypergraph(unittest.TestCase): + """Test cases for the Hypergraph class.""" + + def test_init_basic(self): + """Test basic Hypergraph initialization.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] + graph = Hypergraph(edges) + self.assertEqual(graph.nedges(), 2) + self.assertEqual(graph.nvertices(), 3) + + def test_empty_graph(self): + """Test hypergraph with no edges.""" + graph = Hypergraph([]) + self.assertEqual(graph.nedges(), 0) + self.assertEqual(graph.nvertices(), 0) + + def test_nedges(self): + """Test edge count.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([2, 3])] + graph = Hypergraph(edges) + self.assertEqual(graph.nedges(), 3) + + def test_nvertices(self): + """Test vertex count with unique vertices.""" + edges = [Hyperedge([0, 1]), Hyperedge([2, 3])] + graph = Hypergraph(edges) + self.assertEqual(graph.nvertices(), 4) + + def test_nvertices_with_shared_vertices(self): + """Test vertex count when edges share vertices.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([0, 2])] + graph = Hypergraph(edges) + self.assertEqual(graph.nvertices(), 3) + + def test_vertices_iterator(self): + """Test vertices iterator returns sorted vertices.""" + edges = [Hyperedge([3, 1]), Hyperedge([0, 2])] + graph = Hypergraph(edges) + vertices = list(graph.vertices()) + self.assertEqual(vertices, [0, 1, 2, 3]) + + def test_edges_iterator(self): + """Test edges iterator returns all edges.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] + graph = Hypergraph(edges) + edge_list = list(graph.edges()) + self.assertEqual(len(edge_list), 2) + + def test_edges_with_part_parameter(self): + """Test edges iterator with part parameter (base class ignores it).""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] + graph = Hypergraph(edges) + # Base class returns all edges regardless of part parameter + edge_list_0 = list(graph.edges(part=0)) + edge_list_1 = list(graph.edges(part=1)) + self.assertEqual(len(edge_list_0), 2) + self.assertEqual(len(edge_list_1), 2) + + def test_str(self): + """Test string representation.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([2, 3])] + graph = Hypergraph(edges) + expected = "Hypergraph with 4 vertices and 3 edges." + self.assertEqual(str(graph), expected) + + def test_repr(self): + """Test repr representation.""" + edges = [Hyperedge([0, 1])] + graph = Hypergraph(edges) + result = repr(graph) + self.assertIn("Hypergraph", result) + self.assertIn("Hyperedge", result) + + def test_single_vertex_edges(self): + """Test hypergraph with self-loop edges.""" + edges = [Hyperedge([0]), Hyperedge([1]), Hyperedge([2])] + graph = Hypergraph(edges) + self.assertEqual(graph.nedges(), 3) + self.assertEqual(graph.nvertices(), 3) + + def test_mixed_edge_sizes(self): + """Test hypergraph with edges of different sizes.""" + edges = [ + Hyperedge([0]), # 1 vertex (self-loop) + Hyperedge([1, 2]), # 2 vertices (pair) + Hyperedge([3, 4, 5]), # 3 vertices (triple) + ] + graph = Hypergraph(edges) + self.assertEqual(graph.nedges(), 3) + self.assertEqual(graph.nvertices(), 6) + + def test_non_contiguous_vertices(self): + """Test hypergraph with non-contiguous vertex indices.""" + edges = [Hyperedge([0, 10]), Hyperedge([5, 20])] + graph = Hypergraph(edges) + self.assertEqual(graph.nvertices(), 4) + vertices = list(graph.vertices()) + self.assertEqual(vertices, [0, 5, 10, 20]) + + +if __name__ == "__main__": + unittest.main() From 768107ed36727bd285c75f8e1ebef8fb411a1883 Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 08:48:18 -0800 Subject: [PATCH 02/13] Added greedy edge coloring. Begun restarts. --- .../pip/qsharp/magnets/geometry/hypergraph.py | 109 +++++++++++++++--- 1 file changed, 94 insertions(+), 15 deletions(-) diff --git a/source/pip/qsharp/magnets/geometry/hypergraph.py b/source/pip/qsharp/magnets/geometry/hypergraph.py index 4486ac0e97..da8c288fc1 100644 --- a/source/pip/qsharp/magnets/geometry/hypergraph.py +++ b/source/pip/qsharp/magnets/geometry/hypergraph.py @@ -6,7 +6,8 @@ Hamiltonians, where multi-body interactions can involve more than two sites. """ -from typing import Iterator, List +import random +from typing import Iterator, List, Optional class Hyperedge: @@ -50,7 +51,6 @@ class Hypergraph: Attributes: _edges: List of hyperedges in the order they were added. _vertex_set: Set of all unique vertex indices in the hypergraph. - _edge_list: Set of hyperedges for efficient membership testing. Example: >>> edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([0, 2])] @@ -61,48 +61,127 @@ class Hypergraph: 3 """ - def __init__(self, edges: List[Hyperedge]) -> None: + def __init__(self, edges: List[Hyperedge] = []) -> None: """Initialize a hypergraph with the given edges. Args: edges: List of hyperedges defining the hypergraph structure. """ - self._edges = edges self._vertex_set = set() - self._edge_list = set(edges) + self._edge_list = edges + self.parts = [list(range(len(edges)))] # Single partition by default for edge in edges: self._vertex_set.update(edge.vertices) def nedges(self) -> int: """Return the number of hyperedges in the hypergraph.""" - return len(self._edges) + return len(self._edge_list) def nvertices(self) -> int: """Return the number of vertices in the hypergraph.""" return len(self._vertex_set) + def addEdge(self, edge: Hyperedge, part: int = 0) -> None: + """Add a hyperedge to the hypergraph. + + Args: + edge: The Hyperedge instance to add. + part: Partition index, used for implementations + with edge partitioning for parallel updates. By + default, all edges are added to the single part + with index 0. + """ + self._edge_list.append(edge) + self._vertex_set.update(edge.vertices) + self.parts[part].append(len(self._edge_list) - 1) # Add to specified partition + def vertices(self) -> Iterator[int]: - """Return an iterator over vertices in sorted order. + """Return a list of vertices in sorted order. Returns: - Iterator yielding vertex indices in ascending order. + List of vertex indices in ascending order. """ return iter(sorted(self._vertex_set)) - def edges(self, part: int = 0) -> Iterator[Hyperedge]: - """Return an iterator over hyperedges in the hypergraph. + def edges(self) -> Iterator[Hyperedge]: + """Return a list of all hyperedges in the hypergraph. + + Returns: + List of all hyperedges in the hypergraph. + """ + return iter(self._edge_list) + + def edgesByPart(self, part: int) -> Iterator[Hyperedge]: + """Return a list of hyperedges in the hypergraph. Args: - part: Partition index (reserved for subclass implementations - that support edge partitioning for parallel updates). + part: Partition index, used for implementations + with edge partitioning for parallel updates. By + default, all edges are in a single part with + index 0. Returns: - Iterator over all hyperedges in the hypergraph. + List of all hyperedges in the hypergraph. """ - return iter(self._edge_list) + return iter([self._edge_list[i] for i in self.parts[part]]) def __str__(self) -> str: return f"Hypergraph with {self.nvertices()} vertices and {self.nedges()} edges." def __repr__(self) -> str: - return f"Hypergraph({list(self._edges)})" + return f"Hypergraph({list(self._edge_list)})" + + +def greedyEdgeColoring( + hypergraph: Hypergraph, # The hypergraph to color. + seed: Optional[int] = None, # Random seed for reproducibility. + trials: int = 1, # Number of trials to perform. +) -> Hypergraph: + """Perform a (nondeterministic) greedy edge coloring of the hypergraph. + Args: + hypergraph: The Hypergraph instance to color. + seed: Optional random seed for reproducibility. + trials: Number of trials to perform. The coloring with the fewest colors + will be returned. Default is 1. + + Returns: + A Hypergraph where each (hyper)edge is assigned a color + such that no two (hyper)edges sharing a vertex have the + same color. + """ + best = None + + # To do: parallelize over trials + + for trial in range(trials): + edge_list = hypergraph._edge_list + + # Set random seed for reproducibility + if seed is not None: + random.seed(seed + trial) + + # Shuffle edge indices to randomize coloring order + edge_indexes = list(range(hypergraph.nedges())) + random.shuffle(edge_indexes) + + parts = [ [] ] # Initialize with one empty color part + used_vertices = [ set() ] # Vertices used by each color + for i in range(len(edge_indexes)): + edge = hypergraph._edge_list[edge_indexes[i]] + for j in range(len(parts) + 1): + + + # If we've reached a new color, add it + # Note that if we always match on the last color + if j == len(output.parts): + output.parts.append([]) + used_vertices.append(set()) + + # Check if this edge can be added to color j + # if so, add it and break + if not any(v in used_vertices[j] for v in edge.vertices): + output.parts[j].append(edge_indexes[i]) + used_vertices[j].update(edge.vertices) + break + + return output From 428583eb4b6007f9a108ef26b31979dcc2f2557a Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 08:49:12 -0800 Subject: [PATCH 03/13] Update source/pip/qsharp/magnets/geometry/__init__.py Co-authored-by: Mathias Soeken --- source/pip/qsharp/magnets/geometry/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/pip/qsharp/magnets/geometry/__init__.py b/source/pip/qsharp/magnets/geometry/__init__.py index 53d82f3051..649b2a37b2 100644 --- a/source/pip/qsharp/magnets/geometry/__init__.py +++ b/source/pip/qsharp/magnets/geometry/__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """Geometry module for representing quantum system topologies. This module provides hypergraph data structures for representing the From 6343224ab52495a248ffdab46e01f11043324c55 Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 08:49:27 -0800 Subject: [PATCH 04/13] Update source/pip/qsharp/magnets/geometry/hypergraph.py Co-authored-by: Mathias Soeken --- source/pip/qsharp/magnets/geometry/hypergraph.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/pip/qsharp/magnets/geometry/hypergraph.py b/source/pip/qsharp/magnets/geometry/hypergraph.py index 4486ac0e97..11af95fc9f 100644 --- a/source/pip/qsharp/magnets/geometry/hypergraph.py +++ b/source/pip/qsharp/magnets/geometry/hypergraph.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """Hypergraph data structures for representing quantum system geometries. This module provides classes for representing hypergraphs, which generalize From b7c6fed9a2ddb51eefa522dbaf3f03c99c56a6cf Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 08:56:41 -0800 Subject: [PATCH 05/13] Update source/pip/qsharp/magnets/geometry/hypergraph.py Co-authored-by: Mathias Soeken --- source/pip/qsharp/magnets/geometry/hypergraph.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/pip/qsharp/magnets/geometry/hypergraph.py b/source/pip/qsharp/magnets/geometry/hypergraph.py index 11af95fc9f..b26da6b1ee 100644 --- a/source/pip/qsharp/magnets/geometry/hypergraph.py +++ b/source/pip/qsharp/magnets/geometry/hypergraph.py @@ -26,6 +26,8 @@ class Hyperedge: vertices: Sorted list of vertex indices connected by this hyperedge. Example: + + .. code-block:: python >>> edge = Hyperedge([2, 0, 1]) >>> edge.vertices [0, 1, 2] From 4a20a269634407bbcd19b42cb82003d4423f2c54 Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 08:57:06 -0800 Subject: [PATCH 06/13] Update source/pip/qsharp/magnets/geometry/hypergraph.py Co-authored-by: Mathias Soeken --- source/pip/qsharp/magnets/geometry/hypergraph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/pip/qsharp/magnets/geometry/hypergraph.py b/source/pip/qsharp/magnets/geometry/hypergraph.py index b26da6b1ee..3447c965f5 100644 --- a/source/pip/qsharp/magnets/geometry/hypergraph.py +++ b/source/pip/qsharp/magnets/geometry/hypergraph.py @@ -78,6 +78,7 @@ def __init__(self, edges: List[Hyperedge]) -> None: for edge in edges: self._vertex_set.update(edge.vertices) + @property def nedges(self) -> int: """Return the number of hyperedges in the hypergraph.""" return len(self._edges) From 9c5277628ad675e54087912226361226c492810c Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 08:57:18 -0800 Subject: [PATCH 07/13] Update source/pip/qsharp/magnets/geometry/hypergraph.py Co-authored-by: Mathias Soeken --- source/pip/qsharp/magnets/geometry/hypergraph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/pip/qsharp/magnets/geometry/hypergraph.py b/source/pip/qsharp/magnets/geometry/hypergraph.py index 3447c965f5..ee0715a2de 100644 --- a/source/pip/qsharp/magnets/geometry/hypergraph.py +++ b/source/pip/qsharp/magnets/geometry/hypergraph.py @@ -83,6 +83,7 @@ def nedges(self) -> int: """Return the number of hyperedges in the hypergraph.""" return len(self._edges) + @property def nvertices(self) -> int: """Return the number of vertices in the hypergraph.""" return len(self._vertex_set) From 47a5313cfaac8c88eeef41b74bc4e0fff37b56c8 Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 08:57:38 -0800 Subject: [PATCH 08/13] Update source/pip/qsharp/magnets/geometry/hypergraph.py Co-authored-by: Mathias Soeken --- source/pip/qsharp/magnets/geometry/hypergraph.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/pip/qsharp/magnets/geometry/hypergraph.py b/source/pip/qsharp/magnets/geometry/hypergraph.py index ee0715a2de..e43f56262f 100644 --- a/source/pip/qsharp/magnets/geometry/hypergraph.py +++ b/source/pip/qsharp/magnets/geometry/hypergraph.py @@ -58,6 +58,8 @@ class Hypergraph: _edge_list: Set of hyperedges for efficient membership testing. Example: + + .. code-block:: python >>> edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([0, 2])] >>> graph = Hypergraph(edges) >>> graph.nvertices() From 2190651c608c68c88076d5bd87bb7f111c615b22 Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 08:57:55 -0800 Subject: [PATCH 09/13] Update source/pip/tests/magnets/__init__.py Co-authored-by: Mathias Soeken --- source/pip/tests/magnets/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/pip/tests/magnets/__init__.py b/source/pip/tests/magnets/__init__.py index a424bf492f..686737dba3 100644 --- a/source/pip/tests/magnets/__init__.py +++ b/source/pip/tests/magnets/__init__.py @@ -1 +1,4 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """Unit tests for the magnets library.""" From a6a8e019bb1dac7e12acd4842e088f95b1d6da94 Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 08:58:07 -0800 Subject: [PATCH 10/13] Update source/pip/tests/magnets/test_hypergraph.py Co-authored-by: Mathias Soeken --- source/pip/tests/magnets/test_hypergraph.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/pip/tests/magnets/test_hypergraph.py b/source/pip/tests/magnets/test_hypergraph.py index 9e30dfccbe..2ec8862cbc 100755 --- a/source/pip/tests/magnets/test_hypergraph.py +++ b/source/pip/tests/magnets/test_hypergraph.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """Unit tests for hypergraph data structures.""" import unittest From 0a2201278b351d22e0169c03defd5a40e46dbac2 Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 10:38:06 -0800 Subject: [PATCH 11/13] Updated unit tests. --- .../pip/qsharp/magnets/geometry/hypergraph.py | 10 +- source/pip/tests/magnets/test_hypergraph.py | 292 +++++++++--------- 2 files changed, 158 insertions(+), 144 deletions(-) diff --git a/source/pip/qsharp/magnets/geometry/hypergraph.py b/source/pip/qsharp/magnets/geometry/hypergraph.py index 4486ac0e97..470f61b4ca 100644 --- a/source/pip/qsharp/magnets/geometry/hypergraph.py +++ b/source/pip/qsharp/magnets/geometry/hypergraph.py @@ -6,7 +6,7 @@ Hamiltonians, where multi-body interactions can involve more than two sites. """ -from typing import Iterator, List +from typing import Iterator class Hyperedge: @@ -18,6 +18,8 @@ class Hyperedge: - Single-site terms (self-loops): 1 vertex - Two-body interactions: 2 vertices - Multi-body interactions: 3+ vertices + Each hyperedge is defined by a set of unique vertex indices, which are + stored in sorted order for consistency. Attributes: vertices: Sorted list of vertex indices connected by this hyperedge. @@ -28,13 +30,13 @@ class Hyperedge: [0, 1, 2] """ - def __init__(self, vertices: List[int]) -> None: + def __init__(self, vertices: list[int]) -> None: """Initialize a hyperedge with the given vertices. Args: vertices: List of vertex indices. Will be sorted internally. """ - self.vertices: List[int] = sorted(vertices) + self.vertices: list[int] = sorted(set(vertices)) def __repr__(self) -> str: return f"Hyperedge({self.vertices})" @@ -61,7 +63,7 @@ class Hypergraph: 3 """ - def __init__(self, edges: List[Hyperedge]) -> None: + def __init__(self, edges: list[Hyperedge]) -> None: """Initialize a hypergraph with the given edges. Args: diff --git a/source/pip/tests/magnets/test_hypergraph.py b/source/pip/tests/magnets/test_hypergraph.py index 9e30dfccbe..10ab15b8a7 100755 --- a/source/pip/tests/magnets/test_hypergraph.py +++ b/source/pip/tests/magnets/test_hypergraph.py @@ -1,145 +1,157 @@ """Unit tests for hypergraph data structures.""" -import unittest from qsharp.magnets.geometry.hypergraph import Hyperedge, Hypergraph -class TestHyperedge(unittest.TestCase): - """Test cases for the Hyperedge class.""" - - def test_init_basic(self): - """Test basic Hyperedge initialization.""" - edge = Hyperedge([0, 1]) - self.assertEqual(edge.vertices, [0, 1]) - - def test_vertices_sorted(self): - """Test that vertices are automatically sorted.""" - edge = Hyperedge([3, 1, 2]) - self.assertEqual(edge.vertices, [1, 2, 3]) - - def test_single_vertex(self): - """Test hyperedge with single vertex (self-loop).""" - edge = Hyperedge([5]) - self.assertEqual(edge.vertices, [5]) - self.assertEqual(len(edge.vertices), 1) - - def test_multiple_vertices(self): - """Test hyperedge with multiple vertices (multi-body interaction).""" - edge = Hyperedge([0, 1, 2, 3]) - self.assertEqual(edge.vertices, [0, 1, 2, 3]) - self.assertEqual(len(edge.vertices), 4) - - def test_repr(self): - """Test string representation.""" - edge = Hyperedge([1, 0]) - self.assertEqual(repr(edge), "Hyperedge([0, 1])") - - def test_empty_vertices(self): - """Test hyperedge with empty vertex list.""" - edge = Hyperedge([]) - self.assertEqual(edge.vertices, []) - self.assertEqual(len(edge.vertices), 0) - - -class TestHypergraph(unittest.TestCase): - """Test cases for the Hypergraph class.""" - - def test_init_basic(self): - """Test basic Hypergraph initialization.""" - edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] - graph = Hypergraph(edges) - self.assertEqual(graph.nedges(), 2) - self.assertEqual(graph.nvertices(), 3) - - def test_empty_graph(self): - """Test hypergraph with no edges.""" - graph = Hypergraph([]) - self.assertEqual(graph.nedges(), 0) - self.assertEqual(graph.nvertices(), 0) - - def test_nedges(self): - """Test edge count.""" - edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([2, 3])] - graph = Hypergraph(edges) - self.assertEqual(graph.nedges(), 3) - - def test_nvertices(self): - """Test vertex count with unique vertices.""" - edges = [Hyperedge([0, 1]), Hyperedge([2, 3])] - graph = Hypergraph(edges) - self.assertEqual(graph.nvertices(), 4) - - def test_nvertices_with_shared_vertices(self): - """Test vertex count when edges share vertices.""" - edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([0, 2])] - graph = Hypergraph(edges) - self.assertEqual(graph.nvertices(), 3) - - def test_vertices_iterator(self): - """Test vertices iterator returns sorted vertices.""" - edges = [Hyperedge([3, 1]), Hyperedge([0, 2])] - graph = Hypergraph(edges) - vertices = list(graph.vertices()) - self.assertEqual(vertices, [0, 1, 2, 3]) - - def test_edges_iterator(self): - """Test edges iterator returns all edges.""" - edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] - graph = Hypergraph(edges) - edge_list = list(graph.edges()) - self.assertEqual(len(edge_list), 2) - - def test_edges_with_part_parameter(self): - """Test edges iterator with part parameter (base class ignores it).""" - edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] - graph = Hypergraph(edges) - # Base class returns all edges regardless of part parameter - edge_list_0 = list(graph.edges(part=0)) - edge_list_1 = list(graph.edges(part=1)) - self.assertEqual(len(edge_list_0), 2) - self.assertEqual(len(edge_list_1), 2) - - def test_str(self): - """Test string representation.""" - edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([2, 3])] - graph = Hypergraph(edges) - expected = "Hypergraph with 4 vertices and 3 edges." - self.assertEqual(str(graph), expected) - - def test_repr(self): - """Test repr representation.""" - edges = [Hyperedge([0, 1])] - graph = Hypergraph(edges) - result = repr(graph) - self.assertIn("Hypergraph", result) - self.assertIn("Hyperedge", result) - - def test_single_vertex_edges(self): - """Test hypergraph with self-loop edges.""" - edges = [Hyperedge([0]), Hyperedge([1]), Hyperedge([2])] - graph = Hypergraph(edges) - self.assertEqual(graph.nedges(), 3) - self.assertEqual(graph.nvertices(), 3) - - def test_mixed_edge_sizes(self): - """Test hypergraph with edges of different sizes.""" - edges = [ - Hyperedge([0]), # 1 vertex (self-loop) - Hyperedge([1, 2]), # 2 vertices (pair) - Hyperedge([3, 4, 5]), # 3 vertices (triple) - ] - graph = Hypergraph(edges) - self.assertEqual(graph.nedges(), 3) - self.assertEqual(graph.nvertices(), 6) - - def test_non_contiguous_vertices(self): - """Test hypergraph with non-contiguous vertex indices.""" - edges = [Hyperedge([0, 10]), Hyperedge([5, 20])] - graph = Hypergraph(edges) - self.assertEqual(graph.nvertices(), 4) - vertices = list(graph.vertices()) - self.assertEqual(vertices, [0, 5, 10, 20]) - - -if __name__ == "__main__": - unittest.main() +# Hyperedge tests + + +def test_hyperedge_init_basic(): + """Test basic Hyperedge initialization.""" + edge = Hyperedge([0, 1]) + assert edge.vertices == [0, 1] + + +def test_hyperedge_vertices_sorted(): + """Test that vertices are automatically sorted.""" + edge = Hyperedge([3, 1, 2]) + assert edge.vertices == [1, 2, 3] + + +def test_hyperedge_single_vertex(): + """Test hyperedge with single vertex (self-loop).""" + edge = Hyperedge([5]) + assert edge.vertices == [5] + assert len(edge.vertices) == 1 + + +def test_hyperedge_multiple_vertices(): + """Test hyperedge with multiple vertices (multi-body interaction).""" + edge = Hyperedge([0, 1, 2, 3]) + assert edge.vertices == [0, 1, 2, 3] + assert len(edge.vertices) == 4 + + +def test_hyperedge_repr(): + """Test string representation.""" + edge = Hyperedge([1, 0]) + assert repr(edge) == "Hyperedge([0, 1])" + + +def test_hyperedge_empty_vertices(): + """Test hyperedge with empty vertex list.""" + edge = Hyperedge([]) + assert edge.vertices == [] + assert len(edge.vertices) == 0 + + +# Hypergraph tests + + +def test_hypergraph_init_basic(): + """Test basic Hypergraph initialization.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] + graph = Hypergraph(edges) + assert graph.nedges() == 2 + assert graph.nvertices() == 3 + + +def test_hypergraph_empty_graph(): + """Test hypergraph with no edges.""" + graph = Hypergraph([]) + assert graph.nedges() == 0 + assert graph.nvertices() == 0 + + +def test_hypergraph_nedges(): + """Test edge count.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([2, 3])] + graph = Hypergraph(edges) + assert graph.nedges() == 3 + + +def test_hypergraph_nvertices(): + """Test vertex count with unique vertices.""" + edges = [Hyperedge([0, 1]), Hyperedge([2, 3])] + graph = Hypergraph(edges) + assert graph.nvertices() == 4 + + +def test_hypergraph_nvertices_with_shared_vertices(): + """Test vertex count when edges share vertices.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([0, 2])] + graph = Hypergraph(edges) + assert graph.nvertices() == 3 + + +def test_hypergraph_vertices_iterator(): + """Test vertices iterator returns sorted vertices.""" + edges = [Hyperedge([3, 1]), Hyperedge([0, 2])] + graph = Hypergraph(edges) + vertices = list(graph.vertices()) + assert vertices == [0, 1, 2, 3] + + +def test_hypergraph_edges_iterator(): + """Test edges iterator returns all edges.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] + graph = Hypergraph(edges) + edge_list = list(graph.edges()) + assert len(edge_list) == 2 + + +def test_hypergraph_edges_with_part_parameter(): + """Test edges iterator with part parameter (base class ignores it).""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] + graph = Hypergraph(edges) + # Base class returns all edges regardless of part parameter + edge_list_0 = list(graph.edges(part=0)) + edge_list_1 = list(graph.edges(part=1)) + assert len(edge_list_0) == 2 + assert len(edge_list_1) == 2 + + +def test_hypergraph_str(): + """Test string representation.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([2, 3])] + graph = Hypergraph(edges) + expected = "Hypergraph with 4 vertices and 3 edges." + assert str(graph) == expected + + +def test_hypergraph_repr(): + """Test repr representation.""" + edges = [Hyperedge([0, 1])] + graph = Hypergraph(edges) + result = repr(graph) + assert "Hypergraph" in result + assert "Hyperedge" in result + + +def test_hypergraph_single_vertex_edges(): + """Test hypergraph with self-loop edges.""" + edges = [Hyperedge([0]), Hyperedge([1]), Hyperedge([2])] + graph = Hypergraph(edges) + assert graph.nedges() == 3 + assert graph.nvertices() == 3 + + +def test_hypergraph_mixed_edge_sizes(): + """Test hypergraph with edges of different sizes.""" + edges = [ + Hyperedge([0]), # 1 vertex (self-loop) + Hyperedge([1, 2]), # 2 vertices (pair) + Hyperedge([3, 4, 5]), # 3 vertices (triple) + ] + graph = Hypergraph(edges) + assert graph.nedges() == 3 + assert graph.nvertices() == 6 + + +def test_hypergraph_non_contiguous_vertices(): + """Test hypergraph with non-contiguous vertex indices.""" + edges = [Hyperedge([0, 10]), Hyperedge([5, 20])] + graph = Hypergraph(edges) + assert graph.nvertices() == 4 + vertices = list(graph.vertices()) + assert vertices == [0, 5, 10, 20] From e3e1a3b0d8c170408cf88cddd4fafd9d4e985919 Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 13:47:03 -0800 Subject: [PATCH 12/13] Completed greedy coloring; added simple 1d lattices --- .../pip/qsharp/magnets/geometry/__init__.py | 11 +- .../pip/qsharp/magnets/geometry/hypergraph.py | 93 ++++--- .../pip/qsharp/magnets/geometry/lattice1d.py | 119 +++++++++ source/pip/tests/magnets/test_hypergraph.py | 208 +++++++++++++-- source/pip/tests/magnets/test_lattice1d.py | 236 ++++++++++++++++++ 5 files changed, 617 insertions(+), 50 deletions(-) create mode 100644 source/pip/qsharp/magnets/geometry/lattice1d.py create mode 100644 source/pip/tests/magnets/test_lattice1d.py diff --git a/source/pip/qsharp/magnets/geometry/__init__.py b/source/pip/qsharp/magnets/geometry/__init__.py index 649b2a37b2..7711ad44ea 100644 --- a/source/pip/qsharp/magnets/geometry/__init__.py +++ b/source/pip/qsharp/magnets/geometry/__init__.py @@ -8,6 +8,13 @@ and interaction graphs. """ -from .hypergraph import Hyperedge, Hypergraph +from .hypergraph import Hyperedge, Hypergraph, greedyEdgeColoring +from .lattice1d import Chain1D, Ring1D -__all__ = ["Hyperedge", "Hypergraph"] +__all__ = [ + "Hyperedge", + "Hypergraph", + "greedyEdgeColoring", + "Chain1D", + "Ring1D", +] diff --git a/source/pip/qsharp/magnets/geometry/hypergraph.py b/source/pip/qsharp/magnets/geometry/hypergraph.py index caf8dfb2e0..2442d06642 100644 --- a/source/pip/qsharp/magnets/geometry/hypergraph.py +++ b/source/pip/qsharp/magnets/geometry/hypergraph.py @@ -9,6 +9,7 @@ Hamiltonians, where multi-body interactions can involve more than two sites. """ +from copy import deepcopy import random from typing import Iterator, Optional @@ -56,8 +57,11 @@ class Hypergraph: various lattice geometries used in quantum simulations. Attributes: - _edges: List of hyperedges in the order they were added. + _edge_list: List of hyperedges in the order they were added. _vertex_set: Set of all unique vertex indices in the hypergraph. + parts: List of lists, where each sublist contains indices of edges + belonging to a specific part of an edge partitioning. This is useful + for parallelism in certain architectures. Example: @@ -107,23 +111,23 @@ def addEdge(self, edge: Hyperedge, part: int = 0) -> None: self.parts[part].append(len(self._edge_list) - 1) # Add to specified partition def vertices(self) -> Iterator[int]: - """Return a list of vertices in sorted order. + """Iterate over all vertex indices in the hypergraph. Returns: - List of vertex indices in ascending order. + Iterator of vertex indices in ascending order. """ return iter(sorted(self._vertex_set)) def edges(self) -> Iterator[Hyperedge]: - """Return a list of all hyperedges in the hypergraph. + """Iterate over all hyperedges in the hypergraph. Returns: - List of all hyperedges in the hypergraph. + Iterator of all hyperedges in the hypergraph. """ return iter(self._edge_list) def edgesByPart(self, part: int) -> Iterator[Hyperedge]: - """Return a list of hyperedges in the hypergraph. + """Iterate over hyperedges in a specific partition of the hypergraph. Args: part: Partition index, used for implementations @@ -132,12 +136,12 @@ def edgesByPart(self, part: int) -> Iterator[Hyperedge]: index 0. Returns: - List of all hyperedges in the hypergraph. + Iterator of hyperedges in the specified partition. """ return iter([self._edge_list[i] for i in self.parts[part]]) def __str__(self) -> str: - return f"Hypergraph with {self.nvertices()} vertices and {self.nedges()} edges." + return f"Hypergraph with {self.nvertices} vertices and {self.nedges} edges." def __repr__(self) -> str: return f"Hypergraph({list(self._edge_list)})" @@ -160,39 +164,72 @@ def greedyEdgeColoring( such that no two (hyper)edges sharing a vertex have the same color. """ - best = None - # To do: parallelize over trials + best = Hypergraph(hypergraph._edge_list) # Placeholder for best coloring found + + if seed is not None: + random.seed(seed) + + # Shuffle edge indices to randomize insertion order + edge_indexes = list(range(hypergraph.nedges)) + random.shuffle(edge_indexes) + + best.parts = [[]] # Initialize with one empty color part + used_vertices = [set()] # Vertices used by each color + + for i in range(len(edge_indexes)): + edge = hypergraph._edge_list[edge_indexes[i]] + for j in range(len(best.parts) + 1): + + # If we've reached a new color, add it + if j == len(best.parts): + best.parts.append([]) + used_vertices.append(set()) + + # Check if this edge can be added to color j + # Note that we always match on the last color if it was added + # if so, add it and break + if not any(v in used_vertices[j] for v in edge.vertices): + best.parts[j].append(edge_indexes[i]) + used_vertices[j].update(edge.vertices) + break + + least_colors = len(best.parts) - for trial in range(trials): - edge_list = hypergraph._edge_list + # To do: parallelize over trials + for trial in range(1, trials): # Set random seed for reproducibility + # Designed to work with parallel trials if seed is not None: random.seed(seed + trial) - # Shuffle edge indices to randomize coloring order - edge_indexes = list(range(hypergraph.nedges())) + # Shuffle edge indices to randomize insertion order + edge_indexes = list(range(hypergraph.nedges)) random.shuffle(edge_indexes) - parts = [ [] ] # Initialize with one empty color part - used_vertices = [ set() ] # Vertices used by each color + parts = [[]] # Initialize with one empty color part + used_vertices = [set()] # Vertices used by each color + for i in range(len(edge_indexes)): edge = hypergraph._edge_list[edge_indexes[i]] for j in range(len(parts) + 1): + # If we've reached a new color, add it + if j == len(parts): + parts.append([]) + used_vertices.append(set()) - # If we've reached a new color, add it - # Note that if we always match on the last color - if j == len(output.parts): - output.parts.append([]) - used_vertices.append(set()) + # Check if this edge can be added to color j + # if so, add it and break + if not any(v in used_vertices[j] for v in edge.vertices): + parts[j].append(edge_indexes[i]) + used_vertices[j].update(edge.vertices) + break - # Check if this edge can be added to color j - # if so, add it and break - if not any(v in used_vertices[j] for v in edge.vertices): - output.parts[j].append(edge_indexes[i]) - used_vertices[j].update(edge.vertices) - break + # If this trial used fewer colors, update best + if len(parts) < least_colors: + least_colors = len(parts) + best.parts = deepcopy(parts) - return output + return best diff --git a/source/pip/qsharp/magnets/geometry/lattice1d.py b/source/pip/qsharp/magnets/geometry/lattice1d.py new file mode 100644 index 0000000000..a5a892fff4 --- /dev/null +++ b/source/pip/qsharp/magnets/geometry/lattice1d.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""One-dimensional lattice geometries for quantum simulations. + +This module provides classes for representing 1D lattice structures as +hypergraphs. These lattices are commonly used in quantum spin chain +simulations and other one-dimensional quantum systems. +""" + +from qsharp.magnets.geometry.hypergraph import Hyperedge, Hypergraph + + +class Chain1D(Hypergraph): + """A one-dimensional open chain lattice. + + Represents a linear chain of vertices with nearest-neighbor edges. + The chain has open boundary conditions, meaning the first and last + vertices are not connected. + + The edges are partitioned into two parts for parallel updates: + - Part 0 (if self_loops): Self-loop edges on each vertex + - Part 1: Even-indexed nearest-neighbor edges (0-1, 2-3, ...) + - Part 2: Odd-indexed nearest-neighbor edges (1-2, 3-4, ...) + + Attributes: + length: Number of vertices in the chain. + + Example: + + .. code-block:: python + >>> chain = Chain1D(4) + >>> chain.nvertices + 4 + >>> chain.nedges + 3 + """ + + def __init__(self, length: int, self_loops: bool = False) -> None: + """Initialize a 1D chain lattice. + + Args: + length: Number of vertices in the chain. + self_loops: If True, include self-loop edges on each vertex + for single-site terms. + """ + if self_loops: + _edges = [Hyperedge([i]) for i in range(length)] + else: + _edges = [] + + for i in range(length - 1): + _edges.append(Hyperedge([i, i + 1])) + super().__init__(_edges) + + # Set up edge partitions for parallel updates + if self_loops: + self.parts = [list(range(length - 1))] + else: + self.parts = [] + + self.parts.append(list(range(0, length - 1, 2))) + self.parts.append(list(range(1, length - 1, 2))) + + self.length = length + + +class Ring1D(Hypergraph): + """A one-dimensional ring (periodic chain) lattice. + + Represents a circular chain of vertices with nearest-neighbor edges. + The ring has periodic boundary conditions, meaning the first and last + vertices are connected. + + The edges are partitioned into two parts for parallel updates: + - Part 0 (if self_loops): Self-loop edges on each vertex + - Part 1: Even-indexed nearest-neighbor edges (0-1, 2-3, ...) + - Part 2: Odd-indexed nearest-neighbor edges (1-2, 3-4, ...) + + Attributes: + length: Number of vertices in the ring. + + Example: + + .. code-block:: python + >>> ring = Ring1D(4) + >>> ring.nvertices + 4 + >>> ring.nedges + 4 + """ + + def __init__(self, length: int, self_loops: bool = False) -> None: + """Initialize a 1D ring lattice. + + Args: + length: Number of vertices in the ring. + self_loops: If True, include self-loop edges on each vertex + for single-site terms. + """ + if self_loops: + _edges = [Hyperedge([i]) for i in range(length)] + else: + _edges = [] + + for i in range(length): + _edges.append(Hyperedge([i, (i + 1) % length])) + super().__init__(_edges) + + # Set up edge partitions for parallel updates + if self_loops: + self.parts = [list(range(length))] + else: + self.parts = [] + + self.parts.append(list(range(0, length, 2))) + self.parts.append(list(range(1, length, 2))) + + self.length = length diff --git a/source/pip/tests/magnets/test_hypergraph.py b/source/pip/tests/magnets/test_hypergraph.py index 79f071f47b..654eb99222 100755 --- a/source/pip/tests/magnets/test_hypergraph.py +++ b/source/pip/tests/magnets/test_hypergraph.py @@ -3,7 +3,7 @@ """Unit tests for hypergraph data structures.""" -from qsharp.magnets.geometry.hypergraph import Hyperedge, Hypergraph +from qsharp.magnets.geometry.hypergraph import Hyperedge, Hypergraph, greedyEdgeColoring # Hyperedge tests @@ -48,6 +48,12 @@ def test_hyperedge_empty_vertices(): assert len(edge.vertices) == 0 +def test_hyperedge_duplicate_vertices(): + """Test that duplicate vertices are removed.""" + edge = Hyperedge([1, 2, 2, 1, 3]) + assert edge.vertices == [1, 2, 3] + + # Hypergraph tests @@ -55,36 +61,36 @@ def test_hypergraph_init_basic(): """Test basic Hypergraph initialization.""" edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] graph = Hypergraph(edges) - assert graph.nedges() == 2 - assert graph.nvertices() == 3 + assert graph.nedges == 2 + assert graph.nvertices == 3 def test_hypergraph_empty_graph(): """Test hypergraph with no edges.""" graph = Hypergraph([]) - assert graph.nedges() == 0 - assert graph.nvertices() == 0 + assert graph.nedges == 0 + assert graph.nvertices == 0 def test_hypergraph_nedges(): """Test edge count.""" edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([2, 3])] graph = Hypergraph(edges) - assert graph.nedges() == 3 + assert graph.nedges == 3 def test_hypergraph_nvertices(): """Test vertex count with unique vertices.""" edges = [Hyperedge([0, 1]), Hyperedge([2, 3])] graph = Hypergraph(edges) - assert graph.nvertices() == 4 + assert graph.nvertices == 4 def test_hypergraph_nvertices_with_shared_vertices(): """Test vertex count when edges share vertices.""" edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([0, 2])] graph = Hypergraph(edges) - assert graph.nvertices() == 3 + assert graph.nvertices == 3 def test_hypergraph_vertices_iterator(): @@ -103,15 +109,39 @@ def test_hypergraph_edges_iterator(): assert len(edge_list) == 2 -def test_hypergraph_edges_with_part_parameter(): - """Test edges iterator with part parameter (base class ignores it).""" +def test_hypergraph_edges_by_part(): + """Test edgesByPart returns edges in a specific partition.""" edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] graph = Hypergraph(edges) - # Base class returns all edges regardless of part parameter - edge_list_0 = list(graph.edges(part=0)) - edge_list_1 = list(graph.edges(part=1)) - assert len(edge_list_0) == 2 - assert len(edge_list_1) == 2 + # Default: all edges in part 0 + edge_list = list(graph.edgesByPart(0)) + assert len(edge_list) == 2 + + +def test_hypergraph_add_edge(): + """Test adding an edge to the hypergraph.""" + graph = Hypergraph([]) + graph.addEdge(Hyperedge([0, 1])) + assert graph.nedges == 1 + assert graph.nvertices == 2 + + +def test_hypergraph_add_edge_to_part(): + """Test adding edges to different partitions.""" + graph = Hypergraph([Hyperedge([0, 1])]) + graph.parts.append([]) # Add a second partition + graph.addEdge(Hyperedge([2, 3]), part=1) + assert graph.nedges == 2 + assert len(graph.parts[0]) == 1 + assert len(graph.parts[1]) == 1 + + +def test_hypergraph_parts_default(): + """Test that default parts contain all edge indices.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([2, 3])] + graph = Hypergraph(edges) + assert len(graph.parts) == 1 + assert graph.parts[0] == [0, 1, 2] def test_hypergraph_str(): @@ -135,8 +165,8 @@ def test_hypergraph_single_vertex_edges(): """Test hypergraph with self-loop edges.""" edges = [Hyperedge([0]), Hyperedge([1]), Hyperedge([2])] graph = Hypergraph(edges) - assert graph.nedges() == 3 - assert graph.nvertices() == 3 + assert graph.nedges == 3 + assert graph.nvertices == 3 def test_hypergraph_mixed_edge_sizes(): @@ -147,14 +177,152 @@ def test_hypergraph_mixed_edge_sizes(): Hyperedge([3, 4, 5]), # 3 vertices (triple) ] graph = Hypergraph(edges) - assert graph.nedges() == 3 - assert graph.nvertices() == 6 + assert graph.nedges == 3 + assert graph.nvertices == 6 def test_hypergraph_non_contiguous_vertices(): """Test hypergraph with non-contiguous vertex indices.""" edges = [Hyperedge([0, 10]), Hyperedge([5, 20])] graph = Hypergraph(edges) - assert graph.nvertices() == 4 + assert graph.nvertices == 4 vertices = list(graph.vertices()) assert vertices == [0, 5, 10, 20] + + +# greedyEdgeColoring tests + + +def test_greedy_edge_coloring_empty(): + """Test greedy edge coloring on empty hypergraph.""" + graph = Hypergraph([]) + colored = greedyEdgeColoring(graph) + assert colored.nedges == 0 + assert len(colored.parts) == 1 + assert colored.parts[0] == [] + + +def test_greedy_edge_coloring_single_edge(): + """Test greedy edge coloring with a single edge.""" + graph = Hypergraph([Hyperedge([0, 1])]) + colored = greedyEdgeColoring(graph, seed=42) + assert colored.nedges == 1 + assert len(colored.parts) == 1 + + +def test_greedy_edge_coloring_non_overlapping(): + """Test coloring of non-overlapping edges (can share color).""" + edges = [Hyperedge([0, 1]), Hyperedge([2, 3])] + graph = Hypergraph(edges) + colored = greedyEdgeColoring(graph, seed=42) + # Non-overlapping edges can be in the same color + assert colored.nedges == 2 + assert len(colored.parts) == 1 + + +def test_greedy_edge_coloring_overlapping(): + """Test coloring of overlapping edges (need different colors).""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2])] + graph = Hypergraph(edges) + colored = greedyEdgeColoring(graph, seed=42) + # Overlapping edges need different colors + assert colored.nedges == 2 + assert len(colored.parts) == 2 + + +def test_greedy_edge_coloring_triangle(): + """Test coloring of a triangle (3 edges, all pairwise overlapping).""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([0, 2])] + graph = Hypergraph(edges) + colored = greedyEdgeColoring(graph, seed=42) + # All edges share vertices pairwise, so need 3 colors + assert colored.nedges == 3 + assert len(colored.parts) == 3 + + +def test_greedy_edge_coloring_validity(): + """Test that coloring is valid (no two edges in same part share a vertex).""" + edges = [ + Hyperedge([0, 1]), + Hyperedge([1, 2]), + Hyperedge([2, 3]), + Hyperedge([3, 4]), + Hyperedge([0, 4]), + ] + graph = Hypergraph(edges) + colored = greedyEdgeColoring(graph, seed=42) + + # Verify each part has no overlapping edges + for part in colored.parts: + used_vertices = set() + for edge_idx in part: + edge = colored._edge_list[edge_idx] + # No vertex should already be used in this part + assert not any(v in used_vertices for v in edge.vertices) + used_vertices.update(edge.vertices) + + +def test_greedy_edge_coloring_all_edges_colored(): + """Test that all edges are assigned to exactly one part.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([2, 3])] + graph = Hypergraph(edges) + colored = greedyEdgeColoring(graph, seed=42) + + # Collect all edge indices from all parts + all_colored = [] + for part in colored.parts: + all_colored.extend(part) + + # Should have exactly 3 edges colored, each once + assert sorted(all_colored) == [0, 1, 2] + + +def test_greedy_edge_coloring_reproducible_with_seed(): + """Test that coloring is reproducible with the same seed.""" + edges = [Hyperedge([0, 1]), Hyperedge([1, 2]), Hyperedge([2, 3]), Hyperedge([0, 3])] + graph = Hypergraph(edges) + + colored1 = greedyEdgeColoring(graph, seed=123) + colored2 = greedyEdgeColoring(graph, seed=123) + + assert colored1.parts == colored2.parts + + +def test_greedy_edge_coloring_multiple_trials(): + """Test that multiple trials can find better colorings.""" + edges = [ + Hyperedge([0, 1]), + Hyperedge([1, 2]), + Hyperedge([2, 3]), + Hyperedge([3, 0]), + ] + graph = Hypergraph(edges) + colored = greedyEdgeColoring(graph, seed=42, trials=10) + # A cycle of 4 edges can be 2-colored + assert len(colored.parts) <= 3 # Greedy may not always find optimal + + +def test_greedy_edge_coloring_hyperedges(): + """Test coloring with multi-vertex hyperedges.""" + edges = [ + Hyperedge([0, 1, 2]), + Hyperedge([2, 3, 4]), + Hyperedge([5, 6, 7]), + ] + graph = Hypergraph(edges) + colored = greedyEdgeColoring(graph, seed=42) + + # First two share vertex 2, third is independent + assert colored.nedges == 3 + assert len(colored.parts) >= 2 + + +def test_greedy_edge_coloring_self_loops(): + """Test coloring with self-loop edges.""" + edges = [Hyperedge([0]), Hyperedge([1]), Hyperedge([2])] + graph = Hypergraph(edges) + colored = greedyEdgeColoring(graph, seed=42) + + # Self-loops don't share vertices, can all be same color + assert colored.nedges == 3 + assert len(colored.parts) == 1 diff --git a/source/pip/tests/magnets/test_lattice1d.py b/source/pip/tests/magnets/test_lattice1d.py new file mode 100644 index 0000000000..0161ce9f46 --- /dev/null +++ b/source/pip/tests/magnets/test_lattice1d.py @@ -0,0 +1,236 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Unit tests for 1D lattice data structures.""" + +from qsharp.magnets.geometry.lattice1d import Chain1D, Ring1D + + +# Chain1D tests + + +def test_chain1d_init_basic(): + """Test basic Chain1D initialization.""" + chain = Chain1D(4) + assert chain.nvertices == 4 + assert chain.nedges == 3 + assert chain.length == 4 + + +def test_chain1d_single_vertex(): + """Test Chain1D with a single vertex (no edges).""" + chain = Chain1D(1) + assert chain.nvertices == 0 + assert chain.nedges == 0 + assert chain.length == 1 + + +def test_chain1d_two_vertices(): + """Test Chain1D with two vertices (one edge).""" + chain = Chain1D(2) + assert chain.nvertices == 2 + assert chain.nedges == 1 + + +def test_chain1d_edges(): + """Test that Chain1D creates correct nearest-neighbor edges.""" + chain = Chain1D(4) + edges = list(chain.edges()) + assert len(edges) == 3 + # Check edges are [0,1], [1,2], [2,3] + assert edges[0].vertices == [0, 1] + assert edges[1].vertices == [1, 2] + assert edges[2].vertices == [2, 3] + + +def test_chain1d_vertices(): + """Test that Chain1D vertices are correct.""" + chain = Chain1D(5) + vertices = list(chain.vertices()) + assert vertices == [0, 1, 2, 3, 4] + + +def test_chain1d_with_self_loops(): + """Test Chain1D with self-loops enabled.""" + chain = Chain1D(4, self_loops=True) + assert chain.nvertices == 4 + # 4 self-loops + 3 nearest-neighbor edges = 7 + assert chain.nedges == 7 + + +def test_chain1d_self_loops_edges(): + """Test that self-loop edges are created correctly.""" + chain = Chain1D(3, self_loops=True) + edges = list(chain.edges()) + # First 3 edges should be self-loops + assert edges[0].vertices == [0] + assert edges[1].vertices == [1] + assert edges[2].vertices == [2] + # Next 2 edges should be nearest-neighbor + assert edges[3].vertices == [0, 1] + assert edges[4].vertices == [1, 2] + + +def test_chain1d_parts_without_self_loops(): + """Test edge partitioning without self-loops.""" + chain = Chain1D(5) + # Should have 2 parts: even edges [0,2] and odd edges [1,3] + assert len(chain.parts) == 2 + assert chain.parts[0] == [0, 2] # edges 0-1, 2-3 + assert chain.parts[1] == [1, 3] # edges 1-2, 3-4 + + +def test_chain1d_parts_with_self_loops(): + """Test edge partitioning with self-loops.""" + chain = Chain1D(4, self_loops=True) + # Should have 3 parts: self-loops, even edges, odd edges + assert len(chain.parts) == 3 + + +def test_chain1d_parts_non_overlapping(): + """Test that edges in the same part don't share vertices.""" + chain = Chain1D(6) + for part_indices in chain.parts: + used_vertices = set() + for idx in part_indices: + edge = chain._edge_list[idx] + assert not any(v in used_vertices for v in edge.vertices) + used_vertices.update(edge.vertices) + + +def test_chain1d_str(): + """Test string representation.""" + chain = Chain1D(4) + assert "4 vertices" in str(chain) + assert "3 edges" in str(chain) + + +# Ring1D tests + + +def test_ring1d_init_basic(): + """Test basic Ring1D initialization.""" + ring = Ring1D(4) + assert ring.nvertices == 4 + assert ring.nedges == 4 + assert ring.length == 4 + + +def test_ring1d_two_vertices(): + """Test Ring1D with two vertices (two edges, same pair).""" + ring = Ring1D(2) + assert ring.nvertices == 2 + # Edge 0-1 and edge 1-0 (wrapping), but both are [0,1] after sorting + assert ring.nedges == 2 + + +def test_ring1d_three_vertices(): + """Test Ring1D with three vertices (triangle).""" + ring = Ring1D(3) + assert ring.nvertices == 3 + assert ring.nedges == 3 + + +def test_ring1d_edges(): + """Test that Ring1D creates correct edges including wrap-around.""" + ring = Ring1D(4) + edges = list(ring.edges()) + assert len(edges) == 4 + # Check edges are [0,1], [1,2], [2,3], [0,3] (sorted) + assert edges[0].vertices == [0, 1] + assert edges[1].vertices == [1, 2] + assert edges[2].vertices == [2, 3] + assert edges[3].vertices == [0, 3] # Wrap-around edge + + +def test_ring1d_vertices(): + """Test that Ring1D vertices are correct.""" + ring = Ring1D(5) + vertices = list(ring.vertices()) + assert vertices == [0, 1, 2, 3, 4] + + +def test_ring1d_with_self_loops(): + """Test Ring1D with self-loops enabled.""" + ring = Ring1D(4, self_loops=True) + assert ring.nvertices == 4 + # 4 self-loops + 4 nearest-neighbor edges = 8 + assert ring.nedges == 8 + + +def test_ring1d_self_loops_edges(): + """Test that self-loop edges are created correctly.""" + ring = Ring1D(3, self_loops=True) + edges = list(ring.edges()) + # First 3 edges should be self-loops + assert edges[0].vertices == [0] + assert edges[1].vertices == [1] + assert edges[2].vertices == [2] + # Next 3 edges should be nearest-neighbor (including wrap) + assert edges[3].vertices == [0, 1] + assert edges[4].vertices == [1, 2] + assert edges[5].vertices == [0, 2] # Wrap-around + + +def test_ring1d_parts_without_self_loops(): + """Test edge partitioning without self-loops.""" + ring = Ring1D(4) + # Should have 2 parts for parallel updates + assert len(ring.parts) == 2 + + +def test_ring1d_parts_with_self_loops(): + """Test edge partitioning with self-loops.""" + ring = Ring1D(4, self_loops=True) + # Should have 3 parts: self-loops, even edges, odd edges + assert len(ring.parts) == 3 + + +def test_ring1d_parts_non_overlapping(): + """Test that edges in the same part don't share vertices.""" + ring = Ring1D(6) + for part_indices in ring.parts: + used_vertices = set() + for idx in part_indices: + edge = ring._edge_list[idx] + assert not any(v in used_vertices for v in edge.vertices) + used_vertices.update(edge.vertices) + + +def test_ring1d_str(): + """Test string representation.""" + ring = Ring1D(4) + assert "4 vertices" in str(ring) + assert "4 edges" in str(ring) + + +def test_ring1d_vs_chain1d_edge_count(): + """Test that ring has one more edge than chain of same length.""" + for length in range(2, 10): + chain = Chain1D(length) + ring = Ring1D(length) + assert ring.nedges == chain.nedges + 1 + + +def test_chain1d_inherits_hypergraph(): + """Test that Chain1D is a Hypergraph subclass with all methods.""" + from qsharp.magnets.geometry.hypergraph import Hypergraph + + chain = Chain1D(4) + assert isinstance(chain, Hypergraph) + # Test inherited methods work + assert hasattr(chain, "edges") + assert hasattr(chain, "vertices") + assert hasattr(chain, "edgesByPart") + + +def test_ring1d_inherits_hypergraph(): + """Test that Ring1D is a Hypergraph subclass with all methods.""" + from qsharp.magnets.geometry.hypergraph import Hypergraph + + ring = Ring1D(4) + assert isinstance(ring, Hypergraph) + # Test inherited methods work + assert hasattr(ring, "edges") + assert hasattr(ring, "vertices") + assert hasattr(ring, "edgesByPart") From f1412e525218602e414d771bf3161a9c0099f883 Mon Sep 17 00:00:00 2001 From: Brad Lackey Date: Mon, 2 Feb 2026 16:20:45 -0800 Subject: [PATCH 13/13] Initial implementation plus tests --- source/pip/qsharp/magnets/trotter/__init__.py | 12 + source/pip/qsharp/magnets/trotter/trotter.py | 156 ++++++++++++ source/pip/tests/magnets/test_trotter.py | 241 ++++++++++++++++++ 3 files changed, 409 insertions(+) create mode 100644 source/pip/qsharp/magnets/trotter/__init__.py create mode 100644 source/pip/qsharp/magnets/trotter/trotter.py create mode 100644 source/pip/tests/magnets/test_trotter.py diff --git a/source/pip/qsharp/magnets/trotter/__init__.py b/source/pip/qsharp/magnets/trotter/__init__.py new file mode 100644 index 0000000000..f3107d526a --- /dev/null +++ b/source/pip/qsharp/magnets/trotter/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Trotter-Suzuki methods for time evolution.""" + +from .trotter import TrotterStep, StrangStep, TrotterExpansion + +__all__ = [ + "TrotterStep", + "StrangStep", + "TrotterExpansion", +] diff --git a/source/pip/qsharp/magnets/trotter/trotter.py b/source/pip/qsharp/magnets/trotter/trotter.py new file mode 100644 index 0000000000..b598fe5abf --- /dev/null +++ b/source/pip/qsharp/magnets/trotter/trotter.py @@ -0,0 +1,156 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Base Trotter class for first- and second-order Trotter-Suzuki decomposition.""" + + +class TrotterStep: + """ + Base class for Trotter decompositions. Essentially, this is a wrapper around + a list of (time, term_index) tuples, which specify which term to apply for + how long. + + As a default, the base class implements the first-order Trotter-Suzuki formula + for approximating time evolution under a Hamiltonian represented as a sum of + terms H = ∑_k H_k by sequentially applying each term for the full time + + e^{-i H t} ≈ ∏_k e^{-i H_k t}. + + This base class is designed for lazy evaluation: the list of (time, term_index) + tuples is only generated when the get() method is called. + + Example: + + .. code-block:: python + >>> trotter = TrotterStep(num_terms=3, time=0.5) + >>> trotter.get() + [(0.5, 0), (0.5, 1), (0.5, 2)] + """ + + def __init__(self, num_terms: int, time: float): + """ + Initialize the Trotter decomposition. + + Args: + num_terms: Number of terms in the Hamiltonian + time: Total time for the evolution + """ + self._num_terms = num_terms + self._time_step = time + + def get(self) -> list[tuple[float, int]]: + """ + Get the Trotter decomposition as a list of (time, term_index) tuples. + + Returns: + List of tuples where each tuple contains the time duration and the + index of the term to be applied. + """ + return [(self._time_step, term_index) for term_index in range(self._num_terms)] + + def __str__(self) -> str: + """String representation of the Trotter decomposition.""" + return f"Trotter(time_step={self._time_step}, num_terms={self._num_terms})" + + def __repr__(self) -> str: + """String representation of the Trotter decomposition.""" + return self.__str__() + + +class StrangStep(TrotterStep): + """ + Strang splitting (second-order Trotter-Suzuki decomposition). + + The second-order Trotter formula uses symmetric splitting: + e^{-i H t} ≈ ∏_{k=1}^{n} e^{-i H_k t/2} ∏_{k=n}^{1} e^{-i H_k t/2} + + This provides second-order accuracy in the time step, compared to + first-order for the basic Trotter decomposition. + + Example: + + .. code-block:: python + >>> strang = StrangStep(num_terms=3, time=0.5) + >>> strang.get() + [(0.25, 0), (0.25, 1), (0.5, 2), (0.25, 1), (0.25, 0)] + """ + + def __init__(self, num_terms: int, time: float): + """ + Initialize the Strang splitting. + + Args: + num_terms: Number of terms in the Hamiltonian + time: Total time for the evolution + """ + super().__init__(num_terms, time) + + def get(self) -> list[tuple[float, int]]: + """ + Get the Strang splitting as a list of (time, term_index) tuples. + + Returns: + List of tuples where each tuple contains the time duration and the + index of the term to be applied. The sequence is symmetric for + second-order accuracy. + """ + terms = [] + # Forward sweep with half time steps + for term_index in range(self._num_terms - 1): + terms.append((self._time_step / 2.0, term_index)) + + # Combine the two middle terms + terms.append((self._time_step, self._num_terms - 1)) + + # Backward sweep with half time steps + for term_index in range(self._num_terms - 2, -1, -1): + terms.append((self._time_step / 2.0, term_index)) + + return terms + + def __str__(self) -> str: + """String representation of the Strang splitting.""" + return f"Strang(time_step={self._time_step}, num_terms={self._num_terms})" + + +class TrotterExpansion: + """ + Trotter expansion class for multiple Trotter steps. This class wraps around + a TrotterStep instance and specifies how many times to repeat this Trotter + step. The expansion can be used to represent the full time evolution + as a sequence of Trotter steps + + e^{-i H t} ≈ (∏_k e^{-i H_k t/n})^n. + + where n is the number of Trotter steps. + + Example: + + .. code-block:: python + >>> n = 4 # Number of Trotter steps + >>> total_time = 1.0 # Total time + >>> trotter_expansion = TrotterExpansion(TrotterStep(2, total_time/n), n) + >>> trotter_expansion.get() + [([(0.25, 0), (0.25, 1)], 4)] + """ + + def __init__(self, trotter_step: TrotterStep, num_steps: int): + """ + Initialize the Trotter expansion. + + Args: + trotter_step: An instance of TrotterStep representing a single Trotter step + num_steps: Number of Trotter steps + """ + self._trotter_step = trotter_step + self._num_steps = num_steps + + def get(self) -> list[tuple[list[tuple[float, int]], int]]: + """ + Get the Trotter expansion as a list of (terms, step_index) tuples. + + Returns: + List of tuples where each tuple contains the list of (time, term_index) + for that step and the number of times that step is executed. + """ + return [(self._trotter_step.get(), self._num_steps)] diff --git a/source/pip/tests/magnets/test_trotter.py b/source/pip/tests/magnets/test_trotter.py new file mode 100644 index 0000000000..bd26ed8f72 --- /dev/null +++ b/source/pip/tests/magnets/test_trotter.py @@ -0,0 +1,241 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Unit tests for Trotter-Suzuki decomposition classes.""" + +from qsharp.magnets.trotter import TrotterStep, StrangStep, TrotterExpansion + + +# TrotterStep tests + + +def test_trotter_step_init_basic(): + """Test basic TrotterStep initialization.""" + trotter = TrotterStep(num_terms=3, time=0.5) + assert trotter._num_terms == 3 + assert trotter._time_step == 0.5 + + +def test_trotter_step_get_single_term(): + """Test TrotterStep with a single term.""" + trotter = TrotterStep(num_terms=1, time=1.0) + result = trotter.get() + assert result == [(1.0, 0)] + + +def test_trotter_step_get_multiple_terms(): + """Test TrotterStep with multiple terms.""" + trotter = TrotterStep(num_terms=3, time=0.5) + result = trotter.get() + assert result == [(0.5, 0), (0.5, 1), (0.5, 2)] + + +def test_trotter_step_get_zero_time(): + """Test TrotterStep with zero time.""" + trotter = TrotterStep(num_terms=2, time=0.0) + result = trotter.get() + assert result == [(0.0, 0), (0.0, 1)] + + +def test_trotter_step_get_returns_all_terms(): + """Test that TrotterStep returns all term indices.""" + num_terms = 5 + trotter = TrotterStep(num_terms=num_terms, time=1.0) + result = trotter.get() + assert len(result) == num_terms + term_indices = [idx for _, idx in result] + assert term_indices == list(range(num_terms)) + + +def test_trotter_step_get_uniform_time(): + """Test that all terms have the same time in TrotterStep.""" + time = 0.25 + trotter = TrotterStep(num_terms=4, time=time) + result = trotter.get() + for t, _ in result: + assert t == time + + +def test_trotter_step_str(): + """Test string representation of TrotterStep.""" + trotter = TrotterStep(num_terms=3, time=0.5) + result = str(trotter) + assert "Trotter" in result + assert "0.5" in result + assert "3" in result + + +def test_trotter_step_repr(): + """Test repr representation of TrotterStep.""" + trotter = TrotterStep(num_terms=3, time=0.5) + assert repr(trotter) == str(trotter) + + +# StrangStep tests + + +def test_strang_step_init_basic(): + """Test basic StrangStep initialization.""" + strang = StrangStep(num_terms=3, time=0.5) + assert strang._num_terms == 3 + assert strang._time_step == 0.5 + + +def test_strang_step_inherits_trotter(): + """Test that StrangStep inherits from TrotterStep.""" + strang = StrangStep(num_terms=3, time=0.5) + assert isinstance(strang, TrotterStep) + + +def test_strang_step_get_single_term(): + """Test StrangStep with a single term.""" + strang = StrangStep(num_terms=1, time=1.0) + result = strang.get() + # Single term: just full time on term 0 + assert result == [(1.0, 0)] + + +def test_strang_step_get_two_terms(): + """Test StrangStep with two terms.""" + strang = StrangStep(num_terms=2, time=1.0) + result = strang.get() + # Forward: half on term 0, full on term 1, backward: half on term 0 + assert result == [(0.5, 0), (1.0, 1), (0.5, 0)] + + +def test_strang_step_get_three_terms(): + """Test StrangStep with three terms (example from docstring).""" + strang = StrangStep(num_terms=3, time=0.5) + result = strang.get() + expected = [(0.25, 0), (0.25, 1), (0.5, 2), (0.25, 1), (0.25, 0)] + assert result == expected + + +def test_strang_step_symmetric(): + """Test that StrangStep produces symmetric sequence.""" + strang = StrangStep(num_terms=4, time=1.0) + result = strang.get() + # Check symmetry: term indices should be palindromic + term_indices = [idx for _, idx in result] + assert term_indices == term_indices[::-1] + + +def test_strang_step_time_sum(): + """Test that total time in StrangStep equals expected value.""" + time = 1.0 + num_terms = 3 + strang = StrangStep(num_terms=num_terms, time=time) + result = strang.get() + total_time = sum(t for t, _ in result) + # Each term appears once with full time equivalent + # (half + half for outer terms, full for middle) + assert abs(total_time - time * num_terms) < 1e-10 + + +def test_strang_step_middle_term_full_time(): + """Test that the middle term gets full time step.""" + strang = StrangStep(num_terms=5, time=2.0) + result = strang.get() + # Middle term (index 4, the last term) should have full time + middle_entries = [(t, idx) for t, idx in result if idx == 4] + assert len(middle_entries) == 1 + assert middle_entries[0][0] == 2.0 + + +def test_strang_step_outer_terms_half_time(): + """Test that outer terms get half time steps.""" + strang = StrangStep(num_terms=4, time=2.0) + result = strang.get() + # Term 0 should appear twice with half time each + term_0_entries = [(t, idx) for t, idx in result if idx == 0] + assert len(term_0_entries) == 2 + for t, _ in term_0_entries: + assert t == 1.0 + + +def test_strang_step_str(): + """Test string representation of StrangStep.""" + strang = StrangStep(num_terms=3, time=0.5) + result = str(strang) + assert "Strang" in result + assert "0.5" in result + assert "3" in result + + +# TrotterExpansion tests + + +def test_trotter_expansion_init_basic(): + """Test basic TrotterExpansion initialization.""" + step = TrotterStep(num_terms=2, time=0.25) + expansion = TrotterExpansion(step, num_steps=4) + assert expansion._trotter_step is step + assert expansion._num_steps == 4 + + +def test_trotter_expansion_get_single_step(): + """Test TrotterExpansion with a single step.""" + step = TrotterStep(num_terms=2, time=1.0) + expansion = TrotterExpansion(step, num_steps=1) + result = expansion.get() + assert len(result) == 1 + terms, count = result[0] + assert count == 1 + assert terms == [(1.0, 0), (1.0, 1)] + + +def test_trotter_expansion_get_multiple_steps(): + """Test TrotterExpansion with multiple steps.""" + step = TrotterStep(num_terms=2, time=0.25) + expansion = TrotterExpansion(step, num_steps=4) + result = expansion.get() + assert len(result) == 1 + terms, count = result[0] + assert count == 4 + assert terms == [(0.25, 0), (0.25, 1)] + + +def test_trotter_expansion_with_strang_step(): + """Test TrotterExpansion using StrangStep.""" + step = StrangStep(num_terms=2, time=0.5) + expansion = TrotterExpansion(step, num_steps=2) + result = expansion.get() + assert len(result) == 1 + terms, count = result[0] + assert count == 2 + # StrangStep with 2 terms: [(0.25, 0), (0.5, 1), (0.25, 0)] + assert terms == [(0.25, 0), (0.5, 1), (0.25, 0)] + + +def test_trotter_expansion_total_time(): + """Test that total evolution time is correct.""" + total_time = 1.0 + num_steps = 4 + step = TrotterStep(num_terms=3, time=total_time / num_steps) + expansion = TrotterExpansion(step, num_steps=num_steps) + result = expansion.get() + terms, count = result[0] + # Total time = sum of times in one step * count + step_time = sum(t for t, _ in terms) + total = step_time * count + # For first-order Trotter, step_time = time * num_terms + assert abs(total - total_time * 3) < 1e-10 + + +def test_trotter_expansion_preserves_step(): + """Test that expansion preserves the original step.""" + step = TrotterStep(num_terms=3, time=0.5) + expansion = TrotterExpansion(step, num_steps=10) + result = expansion.get() + terms, _ = result[0] + assert terms == step.get() + + +def test_trotter_expansion_docstring_example(): + """Test the example from the TrotterExpansion docstring.""" + n = 4 # Number of Trotter steps + total_time = 1.0 # Total time + trotter_expansion = TrotterExpansion(TrotterStep(2, total_time / n), n) + result = trotter_expansion.get() + expected = [([(0.25, 0), (0.25, 1)], 4)] + assert result == expected