From d82e652f8890c96c103059b1ec4c6d6a108b4704 Mon Sep 17 00:00:00 2001 From: arettig Date: Mon, 22 Jun 2026 22:54:22 +0000 Subject: [PATCH 1/4] Add nsteps for Trotter expansion based on propagator error The current implementation computes the number of steps needed in a Trotter expansion to reach an error in the ground state energy. This commit adds functionality to compute the number of steps determined by the error in the propagator itself. --- src/openfermion/__init__.py | 2 + src/openfermion/circuits/__init__.py | 2 + src/openfermion/circuits/trotter/__init__.py | 7 ++- .../circuits/trotter/trotter_error.py | 50 ++++++++++++++++--- .../circuits/trotter/trotter_error_test.py | 20 +++++++- 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/openfermion/__init__.py b/src/openfermion/__init__.py index 793dae7e7..f9006c972 100644 --- a/src/openfermion/__init__.py +++ b/src/openfermion/__init__.py @@ -266,11 +266,13 @@ error_bound, error_operator, trotter_steps_required, + trotter_steps_required_propagator, vpe_single_circuit, vpe_circuits_single_timestep, standard_vpe_rotation_set, ) + from openfermion.testing import ( validate_trotterized_evolution, random_interaction_operator_term, diff --git a/src/openfermion/circuits/__init__.py b/src/openfermion/circuits/__init__.py index d596b5c83..1a6b8b2d1 100644 --- a/src/openfermion/circuits/__init__.py +++ b/src/openfermion/circuits/__init__.py @@ -97,8 +97,10 @@ error_bound, error_operator, trotter_steps_required, + trotter_steps_required_propagator, ) + from .vpe_circuits import ( vpe_single_circuit, vpe_circuits_single_timestep, diff --git a/src/openfermion/circuits/trotter/__init__.py b/src/openfermion/circuits/trotter/__init__.py index bc98ad67e..ff82a6b20 100644 --- a/src/openfermion/circuits/trotter/__init__.py +++ b/src/openfermion/circuits/trotter/__init__.py @@ -43,4 +43,9 @@ from .trotter_algorithm import TrotterAlgorithm, TrotterStep -from .trotter_error import error_bound, error_operator, trotter_steps_required +from .trotter_error import ( + error_bound, + error_operator, + trotter_steps_required, + trotter_steps_required_propagator, +) diff --git a/src/openfermion/circuits/trotter/trotter_error.py b/src/openfermion/circuits/trotter/trotter_error.py index 81b47a087..4034da997 100644 --- a/src/openfermion/circuits/trotter/trotter_error.py +++ b/src/openfermion/circuits/trotter/trotter_error.py @@ -178,7 +178,20 @@ def error_bound(terms, tight=False): def trotter_steps_required(trotter_error_bound, time, energy_precision): - """Determine the number of Trotter steps for accurate simulation. + r"""Determine the number of Trotter steps for accurate energy estimation. + + This function calculates the number of steps required to bound the + systematic energy shift (eigenvalue shift) of the effective Hamiltonian + to a given precision. This is appropriate for applications like Quantum + Phase Estimation (QPE) where we only care about the eigenvalues of the + propagator. See appendix B from https://arxiv.org/pdf/1312.1695 + This uses the following definition of error for time t and + number of steps r: + + + $$ + \lvert E - E^{TS} \rvert = \frac{t^2}{r^2} E_{bound}(H) + $$ Args: trotter_error_bound (float): Upper bound on Trotter error in the @@ -187,11 +200,34 @@ def trotter_steps_required(trotter_error_bound, time, energy_precision): energy_precision (float): Acceptable shift in state energy. Returns: - The integer minimum number of Trotter steps required for - simulation to the desired precision. + The integer minimum number of Trotter steps required. + """ + return int(ceil(abs(time) * sqrt(trotter_error_bound / energy_precision))) - Notes: - The number of Trotter steps required is an upper bound on the - true requirement, which may be lower. + +def trotter_steps_required_propagator(trotter_error_bound, time, prop_precision): + r"""Determine the number of Trotter steps for accurate state evolution. + + This function calculates the number of steps required to bound the + error in the time evolution operator (propagator error / spectral norm error) + to a given fidelity precision. This is appropriate for quantum dynamics + applications where we care about the accuracy of the state itself over + time. This uses the following definition of error for time t and + number of steps r: + + $$ + \lVert U - U^{TS} \rVert = \frac{t^3}{r^2} E_{bound}(H) + $$ + + Args: + trotter_error_bound (float): Upper bound on Trotter error in the + state of interest. + time (float): The total simulation time. + prop_precision (float): Acceptable error in the time evolution + operator (propagator error). + + Returns: + The integer minimum number of Trotter steps required. """ - return int(ceil(time * sqrt(trotter_error_bound / energy_precision))) + atime = abs(time) + return int(ceil(atime * sqrt(atime * trotter_error_bound / prop_precision))) diff --git a/src/openfermion/circuits/trotter/trotter_error_test.py b/src/openfermion/circuits/trotter/trotter_error_test.py index 201459747..21fa7da57 100644 --- a/src/openfermion/circuits/trotter/trotter_error_test.py +++ b/src/openfermion/circuits/trotter/trotter_error_test.py @@ -24,6 +24,7 @@ error_operator, error_bound, trotter_steps_required, + trotter_steps_required_propagator, ) @@ -161,8 +162,25 @@ def test_trotter_steps_required(self): def test_trotter_steps_required_negative_time(self): self.assertEqual( - trotter_steps_required(trotter_error_bound=0.1, time=3.3, energy_precision=0.11), 4 + trotter_steps_required(trotter_error_bound=0.1, time=-3.3, energy_precision=0.11), 4 + ) + + def test_trotter_steps_required_propagator(self): + self.assertEqual( + trotter_steps_required_propagator( + trotter_error_bound=0.3, time=2.5, prop_precision=0.04 + ), + 11, + ) + + def test_trotter_steps_required_propagator_negative_time(self): + self.assertEqual( + trotter_steps_required_propagator( + trotter_error_bound=0.1, time=-3.3, prop_precision=0.11 + ), + 6, ) def test_return_type(self): self.assertIsInstance(trotter_steps_required(0.1, 0.1, 0.1), int) + self.assertIsInstance(trotter_steps_required_propagator(0.1, 0.1, 0.1), int) From 15bebc3842aea422b4785eed9c0d7c517bee475b Mon Sep 17 00:00:00 2001 From: arettig Date: Tue, 23 Jun 2026 00:12:21 +0000 Subject: [PATCH 2/4] Ensure parameters to trotter_steps_required make sense Added checks to ensure the parameters and therefore output of trotter_steps_required functions are physically meaningful. Added several tests for edge cases of these functions. --- .../circuits/trotter/trotter_error.py | 12 +++++- .../circuits/trotter/trotter_error_test.py | 43 ++++++++++++++++--- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/openfermion/circuits/trotter/trotter_error.py b/src/openfermion/circuits/trotter/trotter_error.py index 4034da997..122063b93 100644 --- a/src/openfermion/circuits/trotter/trotter_error.py +++ b/src/openfermion/circuits/trotter/trotter_error.py @@ -202,7 +202,11 @@ def trotter_steps_required(trotter_error_bound, time, energy_precision): Returns: The integer minimum number of Trotter steps required. """ - return int(ceil(abs(time) * sqrt(trotter_error_bound / energy_precision))) + if energy_precision <= 0: + raise ValueError("energy_precision must be strictly positive.") + if time == 0: + return 0 + return max(1, int(ceil(abs(time) * sqrt(trotter_error_bound / energy_precision)))) def trotter_steps_required_propagator(trotter_error_bound, time, prop_precision): @@ -229,5 +233,9 @@ def trotter_steps_required_propagator(trotter_error_bound, time, prop_precision) Returns: The integer minimum number of Trotter steps required. """ + if prop_precision <= 0: + raise ValueError("prop_precision must be strictly positive.") + if time == 0: + return 0 atime = abs(time) - return int(ceil(atime * sqrt(atime * trotter_error_bound / prop_precision))) + return max(1, int(ceil(atime * sqrt(atime * trotter_error_bound / prop_precision)))) diff --git a/src/openfermion/circuits/trotter/trotter_error_test.py b/src/openfermion/circuits/trotter/trotter_error_test.py index 21fa7da57..a04d406b7 100644 --- a/src/openfermion/circuits/trotter/trotter_error_test.py +++ b/src/openfermion/circuits/trotter/trotter_error_test.py @@ -159,28 +159,59 @@ def test_trotter_steps_required(self): self.assertEqual( trotter_steps_required(trotter_error_bound=0.3, time=2.5, energy_precision=0.04), 7 ) + self.assertEqual( + trotter_steps_required_propagator( + trotter_error_bound=0.3, time=2.5, prop_precision=0.04 + ), + 11, + ) def test_trotter_steps_required_negative_time(self): self.assertEqual( trotter_steps_required(trotter_error_bound=0.1, time=-3.3, energy_precision=0.11), 4 ) + self.assertEqual( + trotter_steps_required_propagator( + trotter_error_bound=0.1, time=-3.3, prop_precision=0.11 + ), + 6, + ) - def test_trotter_steps_required_propagator(self): + def test_trotter_steps_required_zero_time(self): + self.assertEqual( + trotter_steps_required(trotter_error_bound=0.3, time=0.0, energy_precision=0.04), 0 + ) self.assertEqual( trotter_steps_required_propagator( - trotter_error_bound=0.3, time=2.5, prop_precision=0.04 + trotter_error_bound=0.3, time=0.0, prop_precision=0.04 ), - 11, + 0, ) - def test_trotter_steps_required_propagator_negative_time(self): + def test_trotter_steps_required_zero_error_bound(self): + # Should return at least 1 step for non-zero time even if error bound is 0 + self.assertEqual( + trotter_steps_required(trotter_error_bound=0.0, time=2.5, energy_precision=0.04), 1 + ) self.assertEqual( trotter_steps_required_propagator( - trotter_error_bound=0.1, time=-3.3, prop_precision=0.11 + trotter_error_bound=0.0, time=2.5, prop_precision=0.04 ), - 6, + 1, ) + def test_trotter_steps_required_invalid_precision(self): + with self.assertRaises(ValueError): + trotter_steps_required(trotter_error_bound=0.3, time=2.5, energy_precision=0.0) + with self.assertRaises(ValueError): + trotter_steps_required(trotter_error_bound=0.3, time=2.5, energy_precision=-0.04) + with self.assertRaises(ValueError): + trotter_steps_required_propagator(trotter_error_bound=0.3, time=2.5, prop_precision=0.0) + with self.assertRaises(ValueError): + trotter_steps_required_propagator( + trotter_error_bound=0.3, time=2.5, prop_precision=-0.04 + ) + def test_return_type(self): self.assertIsInstance(trotter_steps_required(0.1, 0.1, 0.1), int) self.assertIsInstance(trotter_steps_required_propagator(0.1, 0.1, 0.1), int) From ce809faca297fbc16d47d783807f063950997e76 Mon Sep 17 00:00:00 2001 From: arettig Date: Tue, 23 Jun 2026 16:34:32 +0000 Subject: [PATCH 3/4] Add check for trotter_error_bound The parameter trotter_error_bound is checked within the trotter_steps_required function and a ValueError is raised if a negative value is provided. --- src/openfermion/circuits/trotter/trotter_error.py | 4 ++++ src/openfermion/circuits/trotter/trotter_error_test.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/openfermion/circuits/trotter/trotter_error.py b/src/openfermion/circuits/trotter/trotter_error.py index 122063b93..f3a375edc 100644 --- a/src/openfermion/circuits/trotter/trotter_error.py +++ b/src/openfermion/circuits/trotter/trotter_error.py @@ -204,6 +204,8 @@ def trotter_steps_required(trotter_error_bound, time, energy_precision): """ if energy_precision <= 0: raise ValueError("energy_precision must be strictly positive.") + if trotter_error_bound < 0: + raise ValueError("trotter_error_bound must be non-negative.") if time == 0: return 0 return max(1, int(ceil(abs(time) * sqrt(trotter_error_bound / energy_precision)))) @@ -235,6 +237,8 @@ def trotter_steps_required_propagator(trotter_error_bound, time, prop_precision) """ if prop_precision <= 0: raise ValueError("prop_precision must be strictly positive.") + if trotter_error_bound < 0: + raise ValueError("trotter_error_bound must be non-negative.") if time == 0: return 0 atime = abs(time) diff --git a/src/openfermion/circuits/trotter/trotter_error_test.py b/src/openfermion/circuits/trotter/trotter_error_test.py index a04d406b7..fd5296d28 100644 --- a/src/openfermion/circuits/trotter/trotter_error_test.py +++ b/src/openfermion/circuits/trotter/trotter_error_test.py @@ -212,6 +212,14 @@ def test_trotter_steps_required_invalid_precision(self): trotter_error_bound=0.3, time=2.5, prop_precision=-0.04 ) + def test_trotter_steps_required_invalid_error_bound(self): + with self.assertRaises(ValueError): + trotter_steps_required(trotter_error_bound=-0.3, time=2.5, energy_precision=0.04) + with self.assertRaises(ValueError): + trotter_steps_required_propagator( + trotter_error_bound=-0.3, time=2.5, prop_precision=0.04 + ) + def test_return_type(self): self.assertIsInstance(trotter_steps_required(0.1, 0.1, 0.1), int) self.assertIsInstance(trotter_steps_required_propagator(0.1, 0.1, 0.1), int) From 5310a8f998c8e44622930bcb0a15fe253afb8d49 Mon Sep 17 00:00:00 2001 From: arettig Date: Tue, 23 Jun 2026 16:44:25 +0000 Subject: [PATCH 4/4] remove added empty lines --- src/openfermion/__init__.py | 1 - src/openfermion/circuits/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/openfermion/__init__.py b/src/openfermion/__init__.py index f9006c972..64a1e1935 100644 --- a/src/openfermion/__init__.py +++ b/src/openfermion/__init__.py @@ -272,7 +272,6 @@ standard_vpe_rotation_set, ) - from openfermion.testing import ( validate_trotterized_evolution, random_interaction_operator_term, diff --git a/src/openfermion/circuits/__init__.py b/src/openfermion/circuits/__init__.py index 1a6b8b2d1..9b22180dc 100644 --- a/src/openfermion/circuits/__init__.py +++ b/src/openfermion/circuits/__init__.py @@ -100,7 +100,6 @@ trotter_steps_required_propagator, ) - from .vpe_circuits import ( vpe_single_circuit, vpe_circuits_single_timestep,