From 6b2e7414d53cce8ddaf448d582e18eb88588b8fc Mon Sep 17 00:00:00 2001 From: James Nelson Date: Tue, 9 Jun 2026 13:16:29 +0100 Subject: [PATCH 1/5] update --- docs/_toc.yml | 1 + qse/operator/operator.py | 33 +++++++++++++++++++++++++++++++++ qse/operator/operators.py | 27 +++++++++++++++++++++++++++ tests/qse/operator_test.py | 25 +++++++++++++++++++++++++ 4 files changed, 86 insertions(+) diff --git a/docs/_toc.yml b/docs/_toc.yml index ff2049bd..92825e60 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -11,6 +11,7 @@ parts: - file: tutorials/generating_lattices - file: tutorials/pulser-calc-example - file: tutorials/finite-geometries + - file: tutorials/operators - file: tutorials/vqe_qiskit - caption: Use-Cases chapters: diff --git a/qse/operator/operator.py b/qse/operator/operator.py index b68df54a..7b5a0709 100644 --- a/qse/operator/operator.py +++ b/qse/operator/operator.py @@ -43,6 +43,12 @@ def __init__(self, operator, qubits, nqbits, coef=1.0): _check_operator(operator) + for q in qubits: + if q < 0 or q >= nqbits: + raise Exception( + "Qubit indices must be between 0 and nqbits-1, inclusive." + ) + if len(qubits) != len(operator): raise Exception( "The number of passed qubits must equal the number of passed operators." @@ -85,11 +91,38 @@ def to_qutip(self): op[qi] = _qutip_converter(op_str) return self.coef * qp.tensor(op) + def copy(self): + """ + Creates a copy of the operator. + + Returns + ------- + Operator + A new instance of Operator with the same attributes. + """ + return Operator(self.operator, self.qubits, self.nqbits, self.coef) + def __repr__(self): return f"{self.coef:.2f} " + " ".join( [f"{op}{q}" for op, q in zip(self.operator, self.qubits)] ) + def __mul__(self, other): + if isinstance(other, (int, float)): + return Operator( + self.operator, self.qubits, self.nqbits, self.coef * other + ) + else: + raise NotImplementedError("Multiplication only supported for scalars.") + + def __imul__(self, other): + if isinstance(other, (int, float)): + self.coef *= other + return self + + def __rmul__(self, other): + return self.__mul__(other) + def _check_operator(op_list): for op in op_list: diff --git a/qse/operator/operators.py b/qse/operator/operators.py index 0c5a5184..af2e7cf1 100644 --- a/qse/operator/operators.py +++ b/qse/operator/operators.py @@ -79,6 +79,17 @@ def extend(self, other): else: raise Exception("other must be Operators or Operator.") + def copy(self): + """ + Creates a copy of the Operators object. + + Returns + ------- + Operators + A copy of the current Operators object. + """ + return Operators(self.operator_list.copy()) + def __add__(self, other): op = self.copy() op += other @@ -98,6 +109,22 @@ def __repr__(self): def __getitem__(self, i): return self.operator_list[i] + def __mul__(self, other): + if isinstance(other, (int, float)): + return Operators([op * other for op in self.operator_list]) + else: + raise NotImplementedError("Multiplication only supported for scalars.") + + def __imul__(self, other): + if isinstance(other, (int, float)): + self.operator_list = [op * other for op in self.operator_list] + return self + else: + raise NotImplementedError("Multiplication only supported for scalars.") + + def __rmul__(self, other): + return self.__mul__(other) + @property def nqbits(self): return self[0].nqbits diff --git a/tests/qse/operator_test.py b/tests/qse/operator_test.py index b4592e63..4747c666 100644 --- a/tests/qse/operator_test.py +++ b/tests/qse/operator_test.py @@ -86,6 +86,19 @@ def test_operator_fail(qubits, operator): qse.Operator(operator, qubits, nqubits=4) +def test_operator_mul(): + """Test multiplying an Operator by a scalar.""" + op = qse.Operator("X", 0, nqbits=2, coef=1.0) + + op_scaled = op * 3.5 + + assert isinstance(op_scaled, qse.Operator) + assert op_scaled.to_str() == "XI" + assert np.isclose(op_scaled.coef, 3.5) + + op *= 2.0 + assert np.isclose(op.coef, 2.0) + def test_operators_init(): """Test initializing empty Operators.""" ops = qse.Operators() @@ -123,3 +136,15 @@ def test_operators_qutip(): op_sum = op1.to_qutip() + op2.to_qutip() assert np.allclose(op_sum.full(), ops.to_qutip().full()) + +def test_operators_mul(): + """Test multiplying Operators by a scalar.""" + nqbits = 4 + ops = qse.Operators([qse.Operator("X", i, nqbits=nqbits+5) for i in range(nqbits)]) + assert all(np.isclose(op.coef, 1.0) for op in ops) + + ops_scaled = ops * 2.5 + assert all(np.isclose(op.coef, 2.5) for op in ops_scaled) + + ops *= 3.5 + assert all(np.isclose(op.coef, 3.5) for op in ops) From ce20a627119c17cb30d80b5d4bcef000437477ac Mon Sep 17 00:00:00 2001 From: James Nelson Date: Tue, 9 Jun 2026 13:25:43 +0100 Subject: [PATCH 2/5] Create operators.ipynb Add tutorial --- docs/tutorials/operators.ipynb | 191 +++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/tutorials/operators.ipynb diff --git a/docs/tutorials/operators.ipynb b/docs/tutorials/operators.ipynb new file mode 100644 index 00000000..0d568512 --- /dev/null +++ b/docs/tutorials/operators.ipynb @@ -0,0 +1,191 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f2f2f320", + "metadata": {}, + "source": [ + "# The Operator and Operators classes\n", + "\n", + "The Operator and Operators classes provide a way to interact with Operators and Hamiltonians." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b08d35b", + "metadata": {}, + "outputs": [], + "source": [ + "import qse" + ] + }, + { + "cell_type": "markdown", + "id": "a042dded", + "metadata": {}, + "source": [ + "## The Operator class\n", + "\n", + "The Operator class represents a single operator acting on a multiple qubit space.\n", + "\n", + "For example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82b62140", + "metadata": {}, + "outputs": [], + "source": [ + "# Create an operator acting with a Pauli X on the \n", + "# third qubit of a 4-qubit system with coefficient 1.0\n", + "qse.Operator(\"X\", qubits=2, nqbits=4, coef=1.0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc3a8639", + "metadata": {}, + "outputs": [], + "source": [ + "# Create an operator acting with a Pauli Z on the \n", + "# first qubit and a Pauli Y on the second qubit of a 3-qubit system with coefficient 4.0\n", + "qse.Operator([\"Z\", \"Y\"], qubits=[0, 1], nqbits=3, coef=4.0)" + ] + }, + { + "cell_type": "markdown", + "id": "5ae6da3e", + "metadata": {}, + "source": [ + "The Operator class supports multiplication, where the coefficient will be multiplied by a scalar." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90458598", + "metadata": {}, + "outputs": [], + "source": [ + "op = qse.Operator(\"X\", qubits=0, nqbits=2)\n", + "print(op)\n", + "\n", + "op *= 3.14\n", + "print(op)" + ] + }, + { + "cell_type": "markdown", + "id": "566f7566", + "metadata": {}, + "source": [ + "## The Operators class\n", + "\n", + "The Operators class is for representing a sum of Operator classes.\n", + "\n", + "For Example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "960dd86d", + "metadata": {}, + "outputs": [], + "source": [ + "op1 = qse.Operator(\"X\", qubits=0, nqbits=2)\n", + "op2 = qse.Operator(\"Y\", qubits=1, nqbits=2)\n", + "ops = qse.Operators([op1, op2])\n", + "ops" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96fe72f8", + "metadata": {}, + "outputs": [], + "source": [ + "ops = qse.Operators([qse.Operator(\"X\", [i, i+1], nqbits=4) for i in range(3)])\n", + "ops" + ] + }, + { + "cell_type": "markdown", + "id": "cad1861a", + "metadata": {}, + "source": [ + "## Creating Operators from Qbits\n", + "Operators can be created from the Qbits class, given an interaction function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7442e079", + "metadata": {}, + "outputs": [], + "source": [ + "spacing = 1.0\n", + "qbits = qse.lattices.ring(spacing, 12)\n", + "\n", + "def distance_func(d):\n", + " # Nearest neighbours interaction\n", + " return -1.*(d < spacing*1.01)\n", + "\n", + "ham_int = qbits.compute_interaction_hamiltonian(\n", + " distance_func=distance_func, interaction=\"Z\"\n", + " )\n", + "ham_int" + ] + }, + { + "cell_type": "markdown", + "id": "10fb5e24", + "metadata": {}, + "source": [ + "We can then combine this with other terms to make complex Hamiltonians. For example, the transverse Ising model:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb9ac4d4", + "metadata": {}, + "outputs": [], + "source": [ + "ham_ext = qse.Operators(\n", + " [qse.Operator(\"X\", i, nqbits=qbits.nqbits) for i in range(qbits.nqbits)]\n", + " )\n", + "\n", + "ham = ham_int + ham_ext\n", + "ham" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qse", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 36c8f4c5a7138df71505ade4095bfcc5fde74fd9 Mon Sep 17 00:00:00 2001 From: James Nelson Date: Tue, 9 Jun 2026 13:34:03 +0100 Subject: [PATCH 3/5] ruff --- qse/operator/operator.py | 8 +++----- qse/operator/operators.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/qse/operator/operator.py b/qse/operator/operator.py index 7b5a0709..f3842304 100644 --- a/qse/operator/operator.py +++ b/qse/operator/operator.py @@ -109,17 +109,15 @@ def __repr__(self): def __mul__(self, other): if isinstance(other, (int, float)): - return Operator( - self.operator, self.qubits, self.nqbits, self.coef * other - ) + return Operator(self.operator, self.qubits, self.nqbits, self.coef * other) else: raise NotImplementedError("Multiplication only supported for scalars.") - + def __imul__(self, other): if isinstance(other, (int, float)): self.coef *= other return self - + def __rmul__(self, other): return self.__mul__(other) diff --git a/qse/operator/operators.py b/qse/operator/operators.py index af2e7cf1..ce1129a9 100644 --- a/qse/operator/operators.py +++ b/qse/operator/operators.py @@ -121,7 +121,7 @@ def __imul__(self, other): return self else: raise NotImplementedError("Multiplication only supported for scalars.") - + def __rmul__(self, other): return self.__mul__(other) From ad4df2f1ca45b39458da7d6856030231a816d9bb Mon Sep 17 00:00:00 2001 From: James Nelson Date: Tue, 9 Jun 2026 13:40:24 +0100 Subject: [PATCH 4/5] ruff format --- docs/tutorials/operators.ipynb | 14 ++++++++------ tests/qse/operator_test.py | 6 +++++- uv.lock | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/tutorials/operators.ipynb b/docs/tutorials/operators.ipynb index 0d568512..658b9274 100644 --- a/docs/tutorials/operators.ipynb +++ b/docs/tutorials/operators.ipynb @@ -39,7 +39,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Create an operator acting with a Pauli X on the \n", + "# Create an operator acting with a Pauli X on the\n", "# third qubit of a 4-qubit system with coefficient 1.0\n", "qse.Operator(\"X\", qubits=2, nqbits=4, coef=1.0)" ] @@ -51,7 +51,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Create an operator acting with a Pauli Z on the \n", + "# Create an operator acting with a Pauli Z on the\n", "# first qubit and a Pauli Y on the second qubit of a 3-qubit system with coefficient 4.0\n", "qse.Operator([\"Z\", \"Y\"], qubits=[0, 1], nqbits=3, coef=4.0)" ] @@ -110,7 +110,7 @@ "metadata": {}, "outputs": [], "source": [ - "ops = qse.Operators([qse.Operator(\"X\", [i, i+1], nqbits=4) for i in range(3)])\n", + "ops = qse.Operators([qse.Operator(\"X\", [i, i + 1], nqbits=4) for i in range(3)])\n", "ops" ] }, @@ -133,13 +133,15 @@ "spacing = 1.0\n", "qbits = qse.lattices.ring(spacing, 12)\n", "\n", + "\n", "def distance_func(d):\n", " # Nearest neighbours interaction\n", - " return -1.*(d < spacing*1.01)\n", + " return -1.0 * (d < spacing * 1.01)\n", + "\n", "\n", "ham_int = qbits.compute_interaction_hamiltonian(\n", " distance_func=distance_func, interaction=\"Z\"\n", - " )\n", + ")\n", "ham_int" ] }, @@ -160,7 +162,7 @@ "source": [ "ham_ext = qse.Operators(\n", " [qse.Operator(\"X\", i, nqbits=qbits.nqbits) for i in range(qbits.nqbits)]\n", - " )\n", + ")\n", "\n", "ham = ham_int + ham_ext\n", "ham" diff --git a/tests/qse/operator_test.py b/tests/qse/operator_test.py index 4747c666..c426101c 100644 --- a/tests/qse/operator_test.py +++ b/tests/qse/operator_test.py @@ -99,6 +99,7 @@ def test_operator_mul(): op *= 2.0 assert np.isclose(op.coef, 2.0) + def test_operators_init(): """Test initializing empty Operators.""" ops = qse.Operators() @@ -137,10 +138,13 @@ def test_operators_qutip(): op_sum = op1.to_qutip() + op2.to_qutip() assert np.allclose(op_sum.full(), ops.to_qutip().full()) + def test_operators_mul(): """Test multiplying Operators by a scalar.""" nqbits = 4 - ops = qse.Operators([qse.Operator("X", i, nqbits=nqbits+5) for i in range(nqbits)]) + ops = qse.Operators( + [qse.Operator("X", i, nqbits=nqbits + 5) for i in range(nqbits)] + ) assert all(np.isclose(op.coef, 1.0) for op in ops) ops_scaled = ops * 2.5 diff --git a/uv.lock b/uv.lock index 8d23dc2b..4fe71dfb 100644 --- a/uv.lock +++ b/uv.lock @@ -4005,7 +4005,7 @@ wheels = [ [[package]] name = "qse" -version = "1.1.14" +version = "1.1.15" source = { editable = "." } dependencies = [ { name = "matplotlib" }, From e48256797f432207c5419fd8dff785c48c1e12ea Mon Sep 17 00:00:00 2001 From: James Nelson Date: Tue, 9 Jun 2026 13:53:56 +0100 Subject: [PATCH 5/5] add extra test --- tests/qse/operator_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/qse/operator_test.py b/tests/qse/operator_test.py index c426101c..7f43c75f 100644 --- a/tests/qse/operator_test.py +++ b/tests/qse/operator_test.py @@ -86,6 +86,13 @@ def test_operator_fail(qubits, operator): qse.Operator(operator, qubits, nqubits=4) +@pytest.mark.parametrize("qubits", [-1, 4, [-1, 2], [3, 4]]) +def test_operator_fail_qubit_index(qubits): + """Test the class raises an error for a qubit outside the index range.""" + with pytest.raises(Exception): + qse.Operator("Z", qubits, nqubits=4) + + def test_operator_mul(): """Test multiplying an Operator by a scalar.""" op = qse.Operator("X", 0, nqbits=2, coef=1.0)