From 84d55b276b4ea6f7f3860f7dc699e6ffc8c77995 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 02:06:56 +0000 Subject: [PATCH 01/42] Move conftest.py to the top level Pytest seems to read the conf file at a different time during execution if it is located at the top of the repo versus in a subdirectory. Moving it the top makes things work more predictably. --- src/openfermion/conftest.py => conftest.py | 5 +++++ 1 file changed, 5 insertions(+) rename src/openfermion/conftest.py => conftest.py (82%) diff --git a/src/openfermion/conftest.py b/conftest.py similarity index 82% rename from src/openfermion/conftest.py rename to conftest.py index 104edd1a1..72802c213 100644 --- a/src/openfermion/conftest.py +++ b/conftest.py @@ -12,11 +12,16 @@ import os import random +import sys from typing import Any import numpy as np import pytest +# Ensure src/ is in sys.path so that the OpenFermion utils module can be +# imported at Pytest startup time. +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "src"))) + def pytest_configure(config: Any) -> None: # Set seeds for collection-time parameterization. From 021f05c11d6d6674659b6f7276e855f2340e1a5f Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 02:07:52 +0000 Subject: [PATCH 02/42] Add pytest fixture to set threadpool limits --- conftest.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/conftest.py b/conftest.py index 72802c213..037f05264 100644 --- a/conftest.py +++ b/conftest.py @@ -36,3 +36,32 @@ def set_random_seed() -> None: """Set a fixed random seed when testing.""" random.seed(0) np.random.seed(0) + + +@pytest.fixture(autouse=True, scope="session") +def set_threadpool_limits(): + """Limit number of threads to prevent oversubscription with pytest-xdist. + + This only has an effect if the Python threadpoolctl package is installed, + and it only influences parallellism in some numerical libraries used in + packages such as NumPy. + """ + try: + import threadpoolctl # type: ignore + except ImportError: + yield + return + + if "PYTEST_XDIST_WORKER_COUNT" in os.environ: + from openfermion.utils import get_available_cpu_count + + try: + n_workers = max(1, int(os.environ["PYTEST_XDIST_WORKER_COUNT"])) + except ValueError: + n_workers = 1 + max_threads_per_worker = max(1, get_available_cpu_count() // n_workers) + # Limit native library thread pools for this worker. + with threadpoolctl.threadpool_limits(limits=max_threads_per_worker): + yield + else: + yield From ca386539739bfd819ea98800fcbff7a964fb1399 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 02:41:46 +0000 Subject: [PATCH 03/42] Count available CPU resources more carefully --- .../linalg/linear_qubit_operator.py | 4 +-- .../linalg/linear_qubit_operator_test.py | 8 ++--- .../thc/utils/thc_factorization.py | 3 +- .../thc/utils/thc_objectives.py | 3 +- src/openfermion/utils/__init__.py | 1 + src/openfermion/utils/operator_utils.py | 30 +++++++++++++++++++ 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/openfermion/linalg/linear_qubit_operator.py b/src/openfermion/linalg/linear_qubit_operator.py index 5a62b87bc..2262da938 100644 --- a/src/openfermion/linalg/linear_qubit_operator.py +++ b/src/openfermion/linalg/linear_qubit_operator.py @@ -20,7 +20,7 @@ import scipy.sparse import scipy.sparse.linalg -from openfermion.utils.operator_utils import count_qubits +from openfermion.utils.operator_utils import count_qubits, get_available_cpu_count class LinearQubitOperatorOptions: @@ -35,7 +35,7 @@ def __init__(self, processes=10, pool=None): if processes <= 0: raise ValueError('Invalid number of processors specified {} <= 0'.format(processes)) - self.processes = min(processes, multiprocessing.cpu_count()) + self.processes = min(processes, get_available_cpu_count()) self.pool = pool def get_processes(self, num): diff --git a/src/openfermion/linalg/linear_qubit_operator_test.py b/src/openfermion/linalg/linear_qubit_operator_test.py index ee3e872c6..918bbf7bc 100644 --- a/src/openfermion/linalg/linear_qubit_operator_test.py +++ b/src/openfermion/linalg/linear_qubit_operator_test.py @@ -11,7 +11,6 @@ # limitations under the License. """Tests for linear_qubit_operator.py.""" -import multiprocessing import unittest import numpy @@ -26,6 +25,7 @@ generate_linear_qubit_operator, ) from openfermion.linalg.sparse_tools import qubit_operator_sparse +from openfermion.utils import get_available_cpu_count class LinearQubitOperatorOptionsTest(unittest.TestCase): @@ -33,7 +33,7 @@ class LinearQubitOperatorOptionsTest(unittest.TestCase): def setUp(self): """LinearQubitOperatorOptions test set up.""" - self.processes = multiprocessing.cpu_count() + self.processes = get_available_cpu_count() self.options = LinearQubitOperatorOptions(self.processes) def test_init(self): @@ -214,13 +214,13 @@ def test_init(self): self.assertEqual(self.linear_operator.n_qubits, self.n_qubits) self.assertIsNone(self.linear_operator.options.pool) - cpu_count = multiprocessing.cpu_count() + cpu_count = get_available_cpu_count() default_processes = min(cpu_count, 10) self.assertEqual(self.linear_operator.options.processes, default_processes) # Generated variables. self.assertEqual( - len(self.linear_operator.qubit_operator_groups), min(multiprocessing.cpu_count(), 3) + len(self.linear_operator.qubit_operator_groups), min(get_available_cpu_count(), 3) ) self.assertEqual( QubitOperator.accumulate(self.linear_operator.qubit_operator_groups), diff --git a/src/openfermion/resource_estimates/thc/utils/thc_factorization.py b/src/openfermion/resource_estimates/thc/utils/thc_factorization.py index 5b283df9e..49ce27404 100644 --- a/src/openfermion/resource_estimates/thc/utils/thc_factorization.py +++ b/src/openfermion/resource_estimates/thc/utils/thc_factorization.py @@ -14,6 +14,7 @@ import jax.numpy as jnp from jax import jit, grad +from openfermion.utils import get_available_cpu_count from .adagrad import adagrad from .thc_objectives import ( thc_objective, @@ -25,7 +26,7 @@ # set mkl thread count for numpy einsum/tensordot calls # leave one CPU un used so we can still access this computer -os.environ["MKL_NUM_THREADS"] = str(max((os.cpu_count() or 1) - 1, 1)) +os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) class CallBackStore: diff --git a/src/openfermion/resource_estimates/thc/utils/thc_objectives.py b/src/openfermion/resource_estimates/thc/utils/thc_objectives.py index 5dc2f0292..b07ac9d03 100644 --- a/src/openfermion/resource_estimates/thc/utils/thc_objectives.py +++ b/src/openfermion/resource_estimates/thc/utils/thc_objectives.py @@ -15,11 +15,12 @@ import numpy.random import numpy.linalg from scipy.optimize import minimize +from openfermion.utils import get_available_cpu_count from .adagrad import adagrad # set mkl thread count for numpy einsum/tensordot calls # leave one CPU un used so we can still access this computer -os.environ["MKL_NUM_THREADS"] = str(max((os.cpu_count() or 1) - 1, 1)) +os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) def thc_objective_jax(xcur, norb, nthc, eri): diff --git a/src/openfermion/utils/__init__.py b/src/openfermion/utils/__init__.py index 5bec19680..0175bf9cf 100644 --- a/src/openfermion/utils/__init__.py +++ b/src/openfermion/utils/__init__.py @@ -32,6 +32,7 @@ from .operator_utils import ( count_qubits, + get_available_cpu_count, get_file_path, hermitian_conjugated, is_hermitian, diff --git a/src/openfermion/utils/operator_utils.py b/src/openfermion/utils/operator_utils.py index 8e55e35cd..f13ceaf38 100644 --- a/src/openfermion/utils/operator_utils.py +++ b/src/openfermion/utils/operator_utils.py @@ -44,6 +44,36 @@ class OperatorSpecificationError(Exception): pass +def get_available_cpu_count() -> int: + """Returns the number of CPU cores available to the current process. + + This function respects active CPU limits such as process affinity, + Docker container limits, and pytest-xdist worker distribution to + prevent oversubscription. + """ + if hasattr(os, "process_cpu_count"): # Python 3.13+ + cpus = os.process_cpu_count() or 1 + elif hasattr(os, "sched_getaffinity"): # Unix/Linux + try: + cpus = len(os.sched_getaffinity(0)) + except Exception: # pylint: disable=broad-exception-caught + cpus = os.cpu_count() or 1 + else: # Fallback for older Python on Windows/macOS + cpus = os.cpu_count() or 1 + + # Divide by the number of active pytest-xdist workers. + if "PYTEST_XDIST_WORKER_COUNT" in os.environ: + try: + worker_count = int(os.environ["PYTEST_XDIST_WORKER_COUNT"]) + if worker_count > 0: + # cpus = max(1, (cpus - 1) // worker_count) + cpus = max(1, cpus // worker_count) + except ValueError: + pass + + return cpus + + def hermitian_conjugated(operator): """Return Hermitian conjugate of operator.""" # Handle FermionOperator From 0967fefe0ede3346e42ae52ab287372baac5ff12 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 02:46:04 +0000 Subject: [PATCH 04/42] Add unit tests for new cpu counting code Thanks to Gemini 3.5 Flash for help. --- src/openfermion/utils/operator_utils_test.py | 122 +++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/src/openfermion/utils/operator_utils_test.py b/src/openfermion/utils/operator_utils_test.py index 015cb0cfc..638556077 100644 --- a/src/openfermion/utils/operator_utils_test.py +++ b/src/openfermion/utils/operator_utils_test.py @@ -634,3 +634,125 @@ def test_file_load(self): filepath = get_file_path(self.filename, self.datadirname) self.assertEqual(filepath, self.datadirname + '/' + self.filename + '.data') + + +class MockOS: + def __init__( + self, process_cpu_count_val=None, sched_getaffinity_val=None, cpu_count_val=4, environ=None + ): + if process_cpu_count_val is not None: + self.process_cpu_count = lambda: process_cpu_count_val + if sched_getaffinity_val is not None: + if isinstance(sched_getaffinity_val, Exception): + + def raise_exc(): + raise sched_getaffinity_val + + self.sched_getaffinity = lambda x: raise_exc() + else: + self.sched_getaffinity = lambda x: sched_getaffinity_val + self.cpu_count = lambda: cpu_count_val + self.environ = environ if environ is not None else {} + + +class GetAvailableCpuCountTest(unittest.TestCase): + + def test_get_available_cpu_count_various_os(self): + from unittest.mock import patch + import openfermion.utils.operator_utils as op_utils + from openfermion.utils.operator_utils import get_available_cpu_count + + # Test Case 1: Python 3.13+ (has process_cpu_count). + mock_os = MockOS(process_cpu_count_val=8) + with patch.object(op_utils, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 8) + + # Test Case 1b: Python 3.13+ returns 0/None -> fallback to 1. + mock_os = MockOS(process_cpu_count_val=0) + with patch.object(op_utils, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 1) + + # Test Case 2: Linux/Unix (no process_cpu_count, has sched_getaffinity). + mock_os = MockOS(sched_getaffinity_val=[1, 2, 3, 4]) + with patch.object(op_utils, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 4) + + # Test Case 3: Linux/Unix but sched_getaffinity raises exception. + mock_os = MockOS(sched_getaffinity_val=ValueError("error"), cpu_count_val=4) + with patch.object(op_utils, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 4) + + # Test Case 4: Windows/macOS/Older Python Fallback. + mock_os = MockOS(cpu_count_val=6) + with patch.object(op_utils, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 6) + + # Test Case 4b: Fallback when cpu_count returns None/0. + mock_os = MockOS(cpu_count_val=0) + with patch.object(op_utils, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 1) + + # Test Case 5: pytest-xdist active with valid worker count. + mock_os = MockOS(cpu_count_val=8, environ={"PYTEST_XDIST_WORKER_COUNT": "4"}) + with patch.object(op_utils, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 2) + + # Test Case 6: pytest-xdist with invalid worker count. + mock_os = MockOS(cpu_count_val=8, environ={"PYTEST_XDIST_WORKER_COUNT": "abc"}) + with patch.object(op_utils, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 8) + + +class SetThreadpoolLimitsTest(unittest.TestCase): + + def test_set_threadpool_limits_fixture(self): + import conftest + import sys + from unittest.mock import patch, MagicMock + + # Test Case 1: threadpoolctl is not installed. + with patch.dict(sys.modules, {"threadpoolctl": None}): + gen = conftest.set_threadpool_limits.__wrapped__() + val = next(gen) + self.assertIsNone(val) + with self.assertRaises(StopIteration): + next(gen) + + # Mock threadpoolctl and os.environ for the remaining cases. + mock_threadpoolctl = MagicMock() + mock_threadpoolctl.threadpool_limits.return_value = MagicMock() + + with patch.dict(sys.modules, {"threadpoolctl": mock_threadpoolctl}): + # Test Case 2: PYTEST_XDIST_WORKER_COUNT is in os.environ. + with patch.dict(os.environ, {"PYTEST_XDIST_WORKER_COUNT": "4"}): + with patch("openfermion.utils.get_available_cpu_count", return_value=8): + gen = conftest.set_threadpool_limits.__wrapped__() + val = next(gen) + self.assertIsNone(val) + mock_threadpoolctl.threadpool_limits.assert_called_once() + mock_threadpoolctl.threadpool_limits.assert_called_with(limits=2) + with self.assertRaises(StopIteration): + next(gen) + mock_threadpoolctl.reset_mock() + + # Test Case 3: PYTEST_XDIST_WORKER_COUNT is in os.environ but invalid. + with patch.dict(os.environ, {"PYTEST_XDIST_WORKER_COUNT": "abc"}): + with patch("openfermion.utils.get_available_cpu_count", return_value=8): + gen = conftest.set_threadpool_limits.__wrapped__() + + val = next(gen) + self.assertIsNone(val) + mock_threadpoolctl.threadpool_limits.assert_called_once() + mock_threadpoolctl.threadpool_limits.assert_called_with(limits=8) + with self.assertRaises(StopIteration): + next(gen) + mock_threadpoolctl.reset_mock() + + # Test Case 4: PYTEST_XDIST_WORKER_COUNT is not in os.environ. + with patch.dict(os.environ, {}, clear=True): + gen = conftest.set_threadpool_limits.__wrapped__() + val = next(gen) + self.assertIsNone(val) + mock_threadpoolctl.threadpool_limits.assert_not_called() + with self.assertRaises(StopIteration): + next(gen) From 632ec444ae1532fd13420d7ee4c892bb044bea8c Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 03:07:37 +0000 Subject: [PATCH 05/42] Make type ignore comments more specific --- conftest.py | 2 +- src/openfermion/circuits/primitives/state_preparation.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conftest.py b/conftest.py index 037f05264..483005827 100644 --- a/conftest.py +++ b/conftest.py @@ -47,7 +47,7 @@ def set_threadpool_limits(): packages such as NumPy. """ try: - import threadpoolctl # type: ignore + import threadpoolctl # type: ignore[import-untyped] except ImportError: yield return diff --git a/src/openfermion/circuits/primitives/state_preparation.py b/src/openfermion/circuits/primitives/state_preparation.py index 220a406f9..4a27e7ee9 100644 --- a/src/openfermion/circuits/primitives/state_preparation.py +++ b/src/openfermion/circuits/primitives/state_preparation.py @@ -95,7 +95,7 @@ def _generic_gaussian_circuit( if isinstance(initial_state, int): initially_occupied_orbitals = _occupied_orbitals(initial_state, n_qubits) else: - initially_occupied_orbitals = initial_state # type: ignore + initially_occupied_orbitals = initial_state # type: ignore[assignment] # Flip bits so that the correct starting orbitals are occupied yield ( @@ -118,7 +118,7 @@ def _spin_symmetric_gaussian_circuit( if isinstance(initial_state, int): initially_occupied_orbitals = _occupied_orbitals(initial_state, n_qubits) else: - initially_occupied_orbitals = initial_state # type: ignore + initially_occupied_orbitals = initial_state # type: ignore[assignment] for spin_sector in range(2): circuit_description, start_orbitals = gaussian_state_preparation_circuit( @@ -190,7 +190,7 @@ def prepare_slater_determinant( if isinstance(initial_state, int): initially_occupied_orbitals = _occupied_orbitals(initial_state, n_qubits) else: - initially_occupied_orbitals = initial_state # type: ignore + initially_occupied_orbitals = initial_state # type: ignore[assignment] # Flip bits so that the first n_occupied are 1 and the rest 0 yield ( From af3d4d863fc7415e8eeeedded83a58e98f980d62 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 03:36:38 +0000 Subject: [PATCH 06/42] Ignore not found import --- conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 483005827..f99b92dd1 100644 --- a/conftest.py +++ b/conftest.py @@ -47,7 +47,7 @@ def set_threadpool_limits(): packages such as NumPy. """ try: - import threadpoolctl # type: ignore[import-untyped] + import threadpoolctl # type: ignore[import-untyped, import-not-found] except ImportError: yield return From 98c417df263a9702916e00c432f84834e95c1747 Mon Sep 17 00:00:00 2001 From: Michael Hucka Date: Wed, 8 Jul 2026 21:43:32 -0700 Subject: [PATCH 07/42] Update src/openfermion/utils/operator_utils_test.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/openfermion/utils/operator_utils_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openfermion/utils/operator_utils_test.py b/src/openfermion/utils/operator_utils_test.py index 638556077..8181492cd 100644 --- a/src/openfermion/utils/operator_utils_test.py +++ b/src/openfermion/utils/operator_utils_test.py @@ -725,7 +725,7 @@ def test_set_threadpool_limits_fixture(self): with patch.dict(sys.modules, {"threadpoolctl": mock_threadpoolctl}): # Test Case 2: PYTEST_XDIST_WORKER_COUNT is in os.environ. with patch.dict(os.environ, {"PYTEST_XDIST_WORKER_COUNT": "4"}): - with patch("openfermion.utils.get_available_cpu_count", return_value=8): + with patch("openfermion.utils.get_available_cpu_count", return_value=2): gen = conftest.set_threadpool_limits.__wrapped__() val = next(gen) self.assertIsNone(val) From 259e36e4622db1b4242e1f8df4f050d787b135b0 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 04:47:24 +0000 Subject: [PATCH 08/42] Fix error in worker calculation As pointed out by Gemini Code Assist, the cpu count was already divided by the xdist worker count. --- conftest.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/conftest.py b/conftest.py index f99b92dd1..e5c8c703e 100644 --- a/conftest.py +++ b/conftest.py @@ -54,14 +54,8 @@ def set_threadpool_limits(): if "PYTEST_XDIST_WORKER_COUNT" in os.environ: from openfermion.utils import get_available_cpu_count - - try: - n_workers = max(1, int(os.environ["PYTEST_XDIST_WORKER_COUNT"])) - except ValueError: - n_workers = 1 - max_threads_per_worker = max(1, get_available_cpu_count() // n_workers) # Limit native library thread pools for this worker. - with threadpoolctl.threadpool_limits(limits=max_threads_per_worker): + with threadpoolctl.threadpool_limits(limits=get_available_cpu_count()): yield else: yield From ac2cdf882f2e4c5c737d8d398309fd3c37e2c6c8 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 04:47:50 +0000 Subject: [PATCH 09/42] Format --- conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conftest.py b/conftest.py index e5c8c703e..34df6b5c7 100644 --- a/conftest.py +++ b/conftest.py @@ -54,6 +54,7 @@ def set_threadpool_limits(): if "PYTEST_XDIST_WORKER_COUNT" in os.environ: from openfermion.utils import get_available_cpu_count + # Limit native library thread pools for this worker. with threadpoolctl.threadpool_limits(limits=get_available_cpu_count()): yield From f5b7795e023b2b5d3cb662af3a5c27956bf73a74 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 17:04:46 +0000 Subject: [PATCH 10/42] Move `get_available_cpu_count()` to config.py The fact that operator_utils.py imported numpy, scipy, and other libraries made it impossible to import `get_available_cpu_count()` alone. This in turn prevented using it to set things before NumPy and other libraries were loaded. --- conftest.py | 2 +- src/openfermion/config.py | 30 ++++ src/openfermion/config_test.py | 140 ++++++++++++++++++ .../linalg/linear_qubit_operator.py | 3 +- .../linalg/linear_qubit_operator_test.py | 2 +- src/openfermion/utils/__init__.py | 1 - src/openfermion/utils/operator_utils.py | 30 ---- src/openfermion/utils/operator_utils_test.py | 122 --------------- 8 files changed, 174 insertions(+), 156 deletions(-) create mode 100644 src/openfermion/config_test.py diff --git a/conftest.py b/conftest.py index 34df6b5c7..6dafd7443 100644 --- a/conftest.py +++ b/conftest.py @@ -53,7 +53,7 @@ def set_threadpool_limits(): return if "PYTEST_XDIST_WORKER_COUNT" in os.environ: - from openfermion.utils import get_available_cpu_count + from openfermion.config import get_available_cpu_count # Limit native library thread pools for this worker. with threadpoolctl.threadpool_limits(limits=get_available_cpu_count()): diff --git a/src/openfermion/config.py b/src/openfermion/config.py index ec9600cf9..ab22adeb2 100644 --- a/src/openfermion/config.py +++ b/src/openfermion/config.py @@ -24,3 +24,33 @@ # Molecular data directory. THIS_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) DATA_DIRECTORY = os.path.realpath(os.path.join(THIS_DIRECTORY, 'testing/data')) + + +def get_available_cpu_count() -> int: + """Returns the number of CPU cores available to the current process. + + This function respects active CPU limits such as process affinity, + Docker container limits, and pytest-xdist worker distribution to + prevent oversubscription. + """ + if hasattr(os, "process_cpu_count"): # Python 3.13+ + cpus = os.process_cpu_count() or 1 + elif hasattr(os, "sched_getaffinity"): # Unix/Linux + try: + cpus = len(os.sched_getaffinity(0)) + except Exception: # pylint: disable=broad-exception-caught + cpus = os.cpu_count() or 1 + else: # Fallback for older Python on Windows/macOS + cpus = os.cpu_count() or 1 + + # Divide by the number of active pytest-xdist workers. + if "PYTEST_XDIST_WORKER_COUNT" in os.environ: + try: + worker_count = int(os.environ["PYTEST_XDIST_WORKER_COUNT"]) + if worker_count > 0: + # cpus = max(1, (cpus - 1) // worker_count) + cpus = max(1, cpus // worker_count) + except ValueError: + pass + + return cpus diff --git a/src/openfermion/config_test.py b/src/openfermion/config_test.py new file mode 100644 index 000000000..966b17004 --- /dev/null +++ b/src/openfermion/config_test.py @@ -0,0 +1,140 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for config.py.""" + +import os +import unittest + + +class MockOS: + def __init__( + self, process_cpu_count_val=None, sched_getaffinity_val=None, cpu_count_val=4, environ=None + ): + if process_cpu_count_val is not None: + self.process_cpu_count = lambda: process_cpu_count_val + if sched_getaffinity_val is not None: + if isinstance(sched_getaffinity_val, Exception): + + def raise_exc(): + raise sched_getaffinity_val + + self.sched_getaffinity = lambda x: raise_exc() + else: + self.sched_getaffinity = lambda x: sched_getaffinity_val + self.cpu_count = lambda: cpu_count_val + self.environ = environ if environ is not None else {} + + +class GetAvailableCpuCountTest(unittest.TestCase): + + def test_get_available_cpu_count_various_os(self): + from unittest.mock import patch + from openfermion.config import get_available_cpu_count + import openfermion.config as config + + # Test Case 1: Python 3.13+ (has process_cpu_count). + mock_os = MockOS(process_cpu_count_val=8) + with patch.object(config, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 8) + + # Test Case 1b: Python 3.13+ returns 0/None -> fallback to 1. + mock_os = MockOS(process_cpu_count_val=0) + with patch.object(config, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 1) + + # Test Case 2: Linux/Unix (no process_cpu_count, has sched_getaffinity). + mock_os = MockOS(sched_getaffinity_val=[1, 2, 3, 4]) + with patch.object(config, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 4) + + # Test Case 3: Linux/Unix but sched_getaffinity raises exception. + mock_os = MockOS(sched_getaffinity_val=ValueError("error"), cpu_count_val=4) + with patch.object(config, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 4) + + # Test Case 4: Windows/macOS/Older Python Fallback. + mock_os = MockOS(cpu_count_val=6) + with patch.object(config, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 6) + + # Test Case 4b: Fallback when cpu_count returns None/0. + mock_os = MockOS(cpu_count_val=0) + with patch.object(config, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 1) + + # Test Case 5: pytest-xdist active with valid worker count. + mock_os = MockOS(cpu_count_val=8, environ={"PYTEST_XDIST_WORKER_COUNT": "4"}) + with patch.object(config, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 2) + + # Test Case 6: pytest-xdist with invalid worker count. + mock_os = MockOS(cpu_count_val=8, environ={"PYTEST_XDIST_WORKER_COUNT": "abc"}) + with patch.object(config, "os", mock_os): + self.assertEqual(get_available_cpu_count(), 8) + + +class SetThreadpoolLimitsTest(unittest.TestCase): + + def test_set_threadpool_limits_fixture(self): + import conftest + import sys + from unittest.mock import patch, MagicMock + + # Test Case 1: threadpoolctl is not installed. + with patch.dict(sys.modules, {"threadpoolctl": None}): + gen = conftest.set_threadpool_limits.__wrapped__() + val = next(gen) + self.assertIsNone(val) + with self.assertRaises(StopIteration): + next(gen) + + # Mock threadpoolctl and os.environ for the remaining cases. + mock_threadpoolctl = MagicMock() + mock_threadpoolctl.threadpool_limits.return_value = MagicMock() + + with patch.dict(sys.modules, {"threadpoolctl": mock_threadpoolctl}): + # Test Case 2: PYTEST_XDIST_WORKER_COUNT is in os.environ. + with patch.dict(os.environ, {"PYTEST_XDIST_WORKER_COUNT": "4"}): + with patch("openfermion.config.get_available_cpu_count", return_value=2): + gen = conftest.set_threadpool_limits.__wrapped__() + val = next(gen) + self.assertIsNone(val) + mock_threadpoolctl.threadpool_limits.assert_called_once() + mock_threadpoolctl.threadpool_limits.assert_called_with(limits=2) + with self.assertRaises(StopIteration): + next(gen) + mock_threadpoolctl.reset_mock() + + # Test Case 3: PYTEST_XDIST_WORKER_COUNT is in os.environ but invalid. + with patch.dict(os.environ, {"PYTEST_XDIST_WORKER_COUNT": "abc"}): + with patch("openfermion.config.get_available_cpu_count", return_value=8): + gen = conftest.set_threadpool_limits.__wrapped__() + + val = next(gen) + self.assertIsNone(val) + mock_threadpoolctl.threadpool_limits.assert_called_once() + mock_threadpoolctl.threadpool_limits.assert_called_with(limits=8) + with self.assertRaises(StopIteration): + next(gen) + mock_threadpoolctl.reset_mock() + + # Test Case 4: PYTEST_XDIST_WORKER_COUNT is not in os.environ. + with patch.dict(os.environ, {}, clear=True): + gen = conftest.set_threadpool_limits.__wrapped__() + val = next(gen) + self.assertIsNone(val) + mock_threadpoolctl.threadpool_limits.assert_not_called() + with self.assertRaises(StopIteration): + next(gen) diff --git a/src/openfermion/linalg/linear_qubit_operator.py b/src/openfermion/linalg/linear_qubit_operator.py index 2262da938..139878f35 100644 --- a/src/openfermion/linalg/linear_qubit_operator.py +++ b/src/openfermion/linalg/linear_qubit_operator.py @@ -20,7 +20,8 @@ import scipy.sparse import scipy.sparse.linalg -from openfermion.utils.operator_utils import count_qubits, get_available_cpu_count +from openfermion.utils.operator_utils import count_qubits +from openfermion.config import get_available_cpu_count class LinearQubitOperatorOptions: diff --git a/src/openfermion/linalg/linear_qubit_operator_test.py b/src/openfermion/linalg/linear_qubit_operator_test.py index 918bbf7bc..7b31113a9 100644 --- a/src/openfermion/linalg/linear_qubit_operator_test.py +++ b/src/openfermion/linalg/linear_qubit_operator_test.py @@ -25,7 +25,7 @@ generate_linear_qubit_operator, ) from openfermion.linalg.sparse_tools import qubit_operator_sparse -from openfermion.utils import get_available_cpu_count +from openfermion.config import get_available_cpu_count class LinearQubitOperatorOptionsTest(unittest.TestCase): diff --git a/src/openfermion/utils/__init__.py b/src/openfermion/utils/__init__.py index 0175bf9cf..5bec19680 100644 --- a/src/openfermion/utils/__init__.py +++ b/src/openfermion/utils/__init__.py @@ -32,7 +32,6 @@ from .operator_utils import ( count_qubits, - get_available_cpu_count, get_file_path, hermitian_conjugated, is_hermitian, diff --git a/src/openfermion/utils/operator_utils.py b/src/openfermion/utils/operator_utils.py index f13ceaf38..8e55e35cd 100644 --- a/src/openfermion/utils/operator_utils.py +++ b/src/openfermion/utils/operator_utils.py @@ -44,36 +44,6 @@ class OperatorSpecificationError(Exception): pass -def get_available_cpu_count() -> int: - """Returns the number of CPU cores available to the current process. - - This function respects active CPU limits such as process affinity, - Docker container limits, and pytest-xdist worker distribution to - prevent oversubscription. - """ - if hasattr(os, "process_cpu_count"): # Python 3.13+ - cpus = os.process_cpu_count() or 1 - elif hasattr(os, "sched_getaffinity"): # Unix/Linux - try: - cpus = len(os.sched_getaffinity(0)) - except Exception: # pylint: disable=broad-exception-caught - cpus = os.cpu_count() or 1 - else: # Fallback for older Python on Windows/macOS - cpus = os.cpu_count() or 1 - - # Divide by the number of active pytest-xdist workers. - if "PYTEST_XDIST_WORKER_COUNT" in os.environ: - try: - worker_count = int(os.environ["PYTEST_XDIST_WORKER_COUNT"]) - if worker_count > 0: - # cpus = max(1, (cpus - 1) // worker_count) - cpus = max(1, cpus // worker_count) - except ValueError: - pass - - return cpus - - def hermitian_conjugated(operator): """Return Hermitian conjugate of operator.""" # Handle FermionOperator diff --git a/src/openfermion/utils/operator_utils_test.py b/src/openfermion/utils/operator_utils_test.py index 8181492cd..015cb0cfc 100644 --- a/src/openfermion/utils/operator_utils_test.py +++ b/src/openfermion/utils/operator_utils_test.py @@ -634,125 +634,3 @@ def test_file_load(self): filepath = get_file_path(self.filename, self.datadirname) self.assertEqual(filepath, self.datadirname + '/' + self.filename + '.data') - - -class MockOS: - def __init__( - self, process_cpu_count_val=None, sched_getaffinity_val=None, cpu_count_val=4, environ=None - ): - if process_cpu_count_val is not None: - self.process_cpu_count = lambda: process_cpu_count_val - if sched_getaffinity_val is not None: - if isinstance(sched_getaffinity_val, Exception): - - def raise_exc(): - raise sched_getaffinity_val - - self.sched_getaffinity = lambda x: raise_exc() - else: - self.sched_getaffinity = lambda x: sched_getaffinity_val - self.cpu_count = lambda: cpu_count_val - self.environ = environ if environ is not None else {} - - -class GetAvailableCpuCountTest(unittest.TestCase): - - def test_get_available_cpu_count_various_os(self): - from unittest.mock import patch - import openfermion.utils.operator_utils as op_utils - from openfermion.utils.operator_utils import get_available_cpu_count - - # Test Case 1: Python 3.13+ (has process_cpu_count). - mock_os = MockOS(process_cpu_count_val=8) - with patch.object(op_utils, "os", mock_os): - self.assertEqual(get_available_cpu_count(), 8) - - # Test Case 1b: Python 3.13+ returns 0/None -> fallback to 1. - mock_os = MockOS(process_cpu_count_val=0) - with patch.object(op_utils, "os", mock_os): - self.assertEqual(get_available_cpu_count(), 1) - - # Test Case 2: Linux/Unix (no process_cpu_count, has sched_getaffinity). - mock_os = MockOS(sched_getaffinity_val=[1, 2, 3, 4]) - with patch.object(op_utils, "os", mock_os): - self.assertEqual(get_available_cpu_count(), 4) - - # Test Case 3: Linux/Unix but sched_getaffinity raises exception. - mock_os = MockOS(sched_getaffinity_val=ValueError("error"), cpu_count_val=4) - with patch.object(op_utils, "os", mock_os): - self.assertEqual(get_available_cpu_count(), 4) - - # Test Case 4: Windows/macOS/Older Python Fallback. - mock_os = MockOS(cpu_count_val=6) - with patch.object(op_utils, "os", mock_os): - self.assertEqual(get_available_cpu_count(), 6) - - # Test Case 4b: Fallback when cpu_count returns None/0. - mock_os = MockOS(cpu_count_val=0) - with patch.object(op_utils, "os", mock_os): - self.assertEqual(get_available_cpu_count(), 1) - - # Test Case 5: pytest-xdist active with valid worker count. - mock_os = MockOS(cpu_count_val=8, environ={"PYTEST_XDIST_WORKER_COUNT": "4"}) - with patch.object(op_utils, "os", mock_os): - self.assertEqual(get_available_cpu_count(), 2) - - # Test Case 6: pytest-xdist with invalid worker count. - mock_os = MockOS(cpu_count_val=8, environ={"PYTEST_XDIST_WORKER_COUNT": "abc"}) - with patch.object(op_utils, "os", mock_os): - self.assertEqual(get_available_cpu_count(), 8) - - -class SetThreadpoolLimitsTest(unittest.TestCase): - - def test_set_threadpool_limits_fixture(self): - import conftest - import sys - from unittest.mock import patch, MagicMock - - # Test Case 1: threadpoolctl is not installed. - with patch.dict(sys.modules, {"threadpoolctl": None}): - gen = conftest.set_threadpool_limits.__wrapped__() - val = next(gen) - self.assertIsNone(val) - with self.assertRaises(StopIteration): - next(gen) - - # Mock threadpoolctl and os.environ for the remaining cases. - mock_threadpoolctl = MagicMock() - mock_threadpoolctl.threadpool_limits.return_value = MagicMock() - - with patch.dict(sys.modules, {"threadpoolctl": mock_threadpoolctl}): - # Test Case 2: PYTEST_XDIST_WORKER_COUNT is in os.environ. - with patch.dict(os.environ, {"PYTEST_XDIST_WORKER_COUNT": "4"}): - with patch("openfermion.utils.get_available_cpu_count", return_value=2): - gen = conftest.set_threadpool_limits.__wrapped__() - val = next(gen) - self.assertIsNone(val) - mock_threadpoolctl.threadpool_limits.assert_called_once() - mock_threadpoolctl.threadpool_limits.assert_called_with(limits=2) - with self.assertRaises(StopIteration): - next(gen) - mock_threadpoolctl.reset_mock() - - # Test Case 3: PYTEST_XDIST_WORKER_COUNT is in os.environ but invalid. - with patch.dict(os.environ, {"PYTEST_XDIST_WORKER_COUNT": "abc"}): - with patch("openfermion.utils.get_available_cpu_count", return_value=8): - gen = conftest.set_threadpool_limits.__wrapped__() - - val = next(gen) - self.assertIsNone(val) - mock_threadpoolctl.threadpool_limits.assert_called_once() - mock_threadpoolctl.threadpool_limits.assert_called_with(limits=8) - with self.assertRaises(StopIteration): - next(gen) - mock_threadpoolctl.reset_mock() - - # Test Case 4: PYTEST_XDIST_WORKER_COUNT is not in os.environ. - with patch.dict(os.environ, {}, clear=True): - gen = conftest.set_threadpool_limits.__wrapped__() - val = next(gen) - self.assertIsNone(val) - mock_threadpoolctl.threadpool_limits.assert_not_called() - with self.assertRaises(StopIteration): - next(gen) From 0031a24e9805558a5aa147069d01e2a01c2e3c2e Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 17:05:09 +0000 Subject: [PATCH 11/42] Set MKL_NUM_THREADS before loading numpy and others --- .../thc/utils/thc_factorization.py | 15 ++++++++++----- .../thc/utils/thc_objectives.py | 15 ++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/openfermion/resource_estimates/thc/utils/thc_factorization.py b/src/openfermion/resource_estimates/thc/utils/thc_factorization.py index 49ce27404..899365b3e 100644 --- a/src/openfermion/resource_estimates/thc/utils/thc_factorization.py +++ b/src/openfermion/resource_estimates/thc/utils/thc_factorization.py @@ -1,6 +1,15 @@ # coverage:ignore # pylint: disable=wrong-import-position import os + +from openfermion.config import get_available_cpu_count + +# Set Intel MKL thread count for NumPy einsum/tensordot calls. +# Needs to be set before other libraries are loaded. +# Reduce count by 1 to lessen impact on host system. +os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) + + from uuid import uuid4 import h5py import numpy @@ -14,7 +23,7 @@ import jax.numpy as jnp from jax import jit, grad -from openfermion.utils import get_available_cpu_count + from .adagrad import adagrad from .thc_objectives import ( thc_objective, @@ -24,10 +33,6 @@ thc_objective_regularized, ) -# set mkl thread count for numpy einsum/tensordot calls -# leave one CPU un used so we can still access this computer -os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) - class CallBackStore: def __init__(self, chkpoint_file, freqency=500): diff --git a/src/openfermion/resource_estimates/thc/utils/thc_objectives.py b/src/openfermion/resource_estimates/thc/utils/thc_objectives.py index b07ac9d03..d3488932d 100644 --- a/src/openfermion/resource_estimates/thc/utils/thc_objectives.py +++ b/src/openfermion/resource_estimates/thc/utils/thc_objectives.py @@ -1,6 +1,15 @@ # coverage:ignore # pylint: disable=wrong-import-position import os + +from openfermion.config import get_available_cpu_count + +# Set Intel MKL thread count for NumPy einsum/tensordot calls. +# Needs to be set before other libraries are loaded. +# Reduce count by 1 to lessen impact on host system. +os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) + + from uuid import uuid4 import scipy.optimize @@ -15,13 +24,9 @@ import numpy.random import numpy.linalg from scipy.optimize import minimize -from openfermion.utils import get_available_cpu_count +from openfermion.config import get_available_cpu_count from .adagrad import adagrad -# set mkl thread count for numpy einsum/tensordot calls -# leave one CPU un used so we can still access this computer -os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) - def thc_objective_jax(xcur, norb, nthc, eri): """ From 173c1ecefba39a10c12fc5da7d63998dc3d6bcdd Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 17:09:45 +0000 Subject: [PATCH 12/42] Resolve pylint errors about import order --- .../resource_estimates/thc/utils/thc_factorization.py | 2 +- src/openfermion/resource_estimates/thc/utils/thc_objectives.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/openfermion/resource_estimates/thc/utils/thc_factorization.py b/src/openfermion/resource_estimates/thc/utils/thc_factorization.py index 899365b3e..f0db7d9a5 100644 --- a/src/openfermion/resource_estimates/thc/utils/thc_factorization.py +++ b/src/openfermion/resource_estimates/thc/utils/thc_factorization.py @@ -1,5 +1,5 @@ # coverage:ignore -# pylint: disable=wrong-import-position +# pylint: disable=wrong-import-position,wrong-import-order import os from openfermion.config import get_available_cpu_count diff --git a/src/openfermion/resource_estimates/thc/utils/thc_objectives.py b/src/openfermion/resource_estimates/thc/utils/thc_objectives.py index d3488932d..d3d41e302 100644 --- a/src/openfermion/resource_estimates/thc/utils/thc_objectives.py +++ b/src/openfermion/resource_estimates/thc/utils/thc_objectives.py @@ -1,5 +1,5 @@ # coverage:ignore -# pylint: disable=wrong-import-position +# pylint: disable=wrong-import-position,wrong-import-order import os from openfermion.config import get_available_cpu_count @@ -24,7 +24,6 @@ import numpy.random import numpy.linalg from scipy.optimize import minimize -from openfermion.config import get_available_cpu_count from .adagrad import adagrad From f08ba3451e69255669e146dd7471050ba23d5b05 Mon Sep 17 00:00:00 2001 From: Michael Hucka Date: Thu, 9 Jul 2026 10:31:02 -0700 Subject: [PATCH 13/42] Update src/openfermion/resource_estimates/thc/utils/thc_factorization.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../resource_estimates/thc/utils/thc_factorization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/openfermion/resource_estimates/thc/utils/thc_factorization.py b/src/openfermion/resource_estimates/thc/utils/thc_factorization.py index f0db7d9a5..419fde6bb 100644 --- a/src/openfermion/resource_estimates/thc/utils/thc_factorization.py +++ b/src/openfermion/resource_estimates/thc/utils/thc_factorization.py @@ -7,7 +7,8 @@ # Set Intel MKL thread count for NumPy einsum/tensordot calls. # Needs to be set before other libraries are loaded. # Reduce count by 1 to lessen impact on host system. -os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) +if "MKL_NUM_THREADS" not in os.environ: + os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) from uuid import uuid4 From d0d1702a43747b18b982a7da72cba5a2e2f6db5f Mon Sep 17 00:00:00 2001 From: Michael Hucka Date: Thu, 9 Jul 2026 10:31:10 -0700 Subject: [PATCH 14/42] Update src/openfermion/resource_estimates/thc/utils/thc_objectives.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/openfermion/resource_estimates/thc/utils/thc_objectives.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/openfermion/resource_estimates/thc/utils/thc_objectives.py b/src/openfermion/resource_estimates/thc/utils/thc_objectives.py index d3d41e302..9ee605322 100644 --- a/src/openfermion/resource_estimates/thc/utils/thc_objectives.py +++ b/src/openfermion/resource_estimates/thc/utils/thc_objectives.py @@ -7,7 +7,8 @@ # Set Intel MKL thread count for NumPy einsum/tensordot calls. # Needs to be set before other libraries are loaded. # Reduce count by 1 to lessen impact on host system. -os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) +if "MKL_NUM_THREADS" not in os.environ: + os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) from uuid import uuid4 From a288b96483c32d169ae907d9fe2a3aece227cf18 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 17:51:37 +0000 Subject: [PATCH 15/42] Fix merge conflicts --- .../resource_estimates/thc/utils/thc_factorization.py | 8 +++----- .../resource_estimates/thc/utils/thc_objectives.py | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/openfermion/resource_estimates/thc/utils/thc_factorization.py b/src/openfermion/resource_estimates/thc/utils/thc_factorization.py index 419fde6bb..26b479482 100644 --- a/src/openfermion/resource_estimates/thc/utils/thc_factorization.py +++ b/src/openfermion/resource_estimates/thc/utils/thc_factorization.py @@ -2,13 +2,11 @@ # pylint: disable=wrong-import-position,wrong-import-order import os -from openfermion.config import get_available_cpu_count +from openfermion.config import set_threading_limits -# Set Intel MKL thread count for NumPy einsum/tensordot calls. +# Set thread limits for NumPy einsum/tensordot calls. # Needs to be set before other libraries are loaded. -# Reduce count by 1 to lessen impact on host system. -if "MKL_NUM_THREADS" not in os.environ: - os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) +set_threading_limits() from uuid import uuid4 diff --git a/src/openfermion/resource_estimates/thc/utils/thc_objectives.py b/src/openfermion/resource_estimates/thc/utils/thc_objectives.py index 9ee605322..1e893f8b9 100644 --- a/src/openfermion/resource_estimates/thc/utils/thc_objectives.py +++ b/src/openfermion/resource_estimates/thc/utils/thc_objectives.py @@ -2,13 +2,11 @@ # pylint: disable=wrong-import-position,wrong-import-order import os -from openfermion.config import get_available_cpu_count +from openfermion.config import set_threading_limits -# Set Intel MKL thread count for NumPy einsum/tensordot calls. +# Set thread limits for NumPy einsum/tensordot calls. # Needs to be set before other libraries are loaded. -# Reduce count by 1 to lessen impact on host system. -if "MKL_NUM_THREADS" not in os.environ: - os.environ["MKL_NUM_THREADS"] = str(max(get_available_cpu_count() - 1, 1)) +set_threading_limits() from uuid import uuid4 From 40ba3e8027a1946d4c8dc16e276de1035cc06275 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 17:50:01 +0000 Subject: [PATCH 16/42] Set OMP_NUM_THREADS and OPENBLAS_NUM_THREADS too --- src/openfermion/config.py | 12 ++++++++++++ src/openfermion/config_test.py | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/openfermion/config.py b/src/openfermion/config.py index ab22adeb2..1fd43d41d 100644 --- a/src/openfermion/config.py +++ b/src/openfermion/config.py @@ -54,3 +54,15 @@ def get_available_cpu_count() -> int: pass return cpus + + +def set_threading_limits() -> None: + """Sets thread limits for underlying numerical libraries (MKL, OpenBLAS, OMP). + + This prevents libraries like NumPy, SciPy, and JAX from oversubscribing + available CPU resources on high-core-count or multi-worker environments. + """ + limit = str(max(get_available_cpu_count() - 1, 1)) + for var in ["MKL_NUM_THREADS", "OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS"]: + if var not in os.environ: + os.environ[var] = limit diff --git a/src/openfermion/config_test.py b/src/openfermion/config_test.py index 966b17004..5224046f7 100644 --- a/src/openfermion/config_test.py +++ b/src/openfermion/config_test.py @@ -138,3 +138,28 @@ def test_set_threadpool_limits_fixture(self): mock_threadpoolctl.threadpool_limits.assert_not_called() with self.assertRaises(StopIteration): next(gen) + + +class SetThreadingLimitsTest(unittest.TestCase): + + def test_set_threading_limits(self): + from openfermion.config import set_threading_limits + from unittest.mock import patch + + # Clean/mocked environment dictionary. + env = {} + with patch.dict(os.environ, env, clear=True): + with patch("openfermion.config.get_available_cpu_count", return_value=8): + set_threading_limits() + self.assertEqual(os.environ.get("MKL_NUM_THREADS"), "7") + self.assertEqual(os.environ.get("OMP_NUM_THREADS"), "7") + self.assertEqual(os.environ.get("OPENBLAS_NUM_THREADS"), "7") + + # Test respecting existing values. + env = {"OMP_NUM_THREADS": "3"} + with patch.dict(os.environ, env, clear=True): + with patch("openfermion.config.get_available_cpu_count", return_value=8): + set_threading_limits() + self.assertEqual(os.environ.get("MKL_NUM_THREADS"), "7") + self.assertEqual(os.environ.get("OMP_NUM_THREADS"), "3") # Respected! + self.assertEqual(os.environ.get("OPENBLAS_NUM_THREADS"), "7") From a9578069d50eed7f79db318493e82072615074f2 Mon Sep 17 00:00:00 2001 From: Michael Hucka Date: Thu, 9 Jul 2026 10:56:15 -0700 Subject: [PATCH 17/42] Update src/openfermion/config.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/openfermion/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/openfermion/config.py b/src/openfermion/config.py index 1fd43d41d..c12400d8b 100644 --- a/src/openfermion/config.py +++ b/src/openfermion/config.py @@ -62,7 +62,10 @@ def set_threading_limits() -> None: This prevents libraries like NumPy, SciPy, and JAX from oversubscribing available CPU resources on high-core-count or multi-worker environments. """ - limit = str(max(get_available_cpu_count() - 1, 1)) + if "PYTEST_XDIST_WORKER_COUNT" in os.environ: + limit = str(max(get_available_cpu_count(), 1)) + else: + limit = str(max(get_available_cpu_count() - 1, 1)) for var in ["MKL_NUM_THREADS", "OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS"]: if var not in os.environ: os.environ[var] = limit From 8a278eeba63f280fe9187180b3d5259665ee8523 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 18:02:42 +0000 Subject: [PATCH 18/42] Avoid inefficiency when num processes < 1 --- src/openfermion/linalg/linear_qubit_operator.py | 3 +++ src/openfermion/linalg/linear_qubit_operator_test.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/src/openfermion/linalg/linear_qubit_operator.py b/src/openfermion/linalg/linear_qubit_operator.py index 139878f35..6395b8972 100644 --- a/src/openfermion/linalg/linear_qubit_operator.py +++ b/src/openfermion/linalg/linear_qubit_operator.py @@ -186,6 +186,9 @@ def _matvec(self, x): if not self.linear_operators: return numpy.zeros(x.shape) + if self.options.processes <= 1: + return functools.reduce(numpy.add, (operator * x for operator in self.linear_operators)) + pool = self.options.get_pool(len(self.linear_operators)) vecs = pool.imap_unordered( apply_operator, [(operator, x) for operator in self.linear_operators] diff --git a/src/openfermion/linalg/linear_qubit_operator_test.py b/src/openfermion/linalg/linear_qubit_operator_test.py index 7b31113a9..cd9f7d005 100644 --- a/src/openfermion/linalg/linear_qubit_operator_test.py +++ b/src/openfermion/linalg/linear_qubit_operator_test.py @@ -262,6 +262,15 @@ def test_closed_workers_not_reused(self): parallel_qubit_op.dot(state) self.assertIsNone(parallel_qubit_op.options.pool) + def test_matvec_single_process(self): + """Tests that when processes is 1, it computes correctly and doesn't crash.""" + qubit_operator = QubitOperator('Z3') + QubitOperator('Y0') + QubitOperator('X1') + options = LinearQubitOperatorOptions(processes=1) + parallel_qubit_op = ParallelLinearQubitOperator( + qubit_operator, self.n_qubits, options=options + ) + self.assertTrue(numpy.allclose(parallel_qubit_op * self.vec, self.expected_matvec)) + class UtilityFunctionTest(unittest.TestCase): """Tests for utility functions.""" From 2768ebbd168f642dfa7f7d2cee760b7fcd6b30fc Mon Sep 17 00:00:00 2001 From: Michael Hucka Date: Thu, 9 Jul 2026 11:07:27 -0700 Subject: [PATCH 19/42] Update src/openfermion/config.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/openfermion/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/openfermion/config.py b/src/openfermion/config.py index c12400d8b..781b52bed 100644 --- a/src/openfermion/config.py +++ b/src/openfermion/config.py @@ -48,7 +48,6 @@ def get_available_cpu_count() -> int: try: worker_count = int(os.environ["PYTEST_XDIST_WORKER_COUNT"]) if worker_count > 0: - # cpus = max(1, (cpus - 1) // worker_count) cpus = max(1, cpus // worker_count) except ValueError: pass From 6f8fdff8a7e11aaff3285288c3df48ca25c7b544 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 18:23:59 +0000 Subject: [PATCH 20/42] Set threading limits here too --- conftest.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/conftest.py b/conftest.py index 6dafd7443..31caabe2c 100644 --- a/conftest.py +++ b/conftest.py @@ -1,3 +1,4 @@ +# pylint: disable=wrong-import-position,wrong-import-order # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -11,17 +12,22 @@ # limitations under the License. import os -import random import sys -from typing import Any - -import numpy as np -import pytest # Ensure src/ is in sys.path so that the OpenFermion utils module can be # imported at Pytest startup time. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "src"))) +from openfermion.config import set_threading_limits + +set_threading_limits() + +import random +from typing import Any + +import numpy as np +import pytest + def pytest_configure(config: Any) -> None: # Set seeds for collection-time parameterization. From 81ae963d324d71d81838ac95d39aa416c64208c0 Mon Sep 17 00:00:00 2001 From: mhucka Date: Thu, 9 Jul 2026 18:48:07 +0000 Subject: [PATCH 21/42] Add more test coverage --- src/openfermion/config_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/openfermion/config_test.py b/src/openfermion/config_test.py index 5224046f7..0158ab8e6 100644 --- a/src/openfermion/config_test.py +++ b/src/openfermion/config_test.py @@ -163,3 +163,12 @@ def test_set_threading_limits(self): self.assertEqual(os.environ.get("MKL_NUM_THREADS"), "7") self.assertEqual(os.environ.get("OMP_NUM_THREADS"), "3") # Respected! self.assertEqual(os.environ.get("OPENBLAS_NUM_THREADS"), "7") + + # Test when PYTEST_XDIST_WORKER_COUNT is in os.environ (line 65 coverage). + env = {"PYTEST_XDIST_WORKER_COUNT": "4"} + with patch.dict(os.environ, env, clear=True): + with patch("openfermion.config.get_available_cpu_count", return_value=8): + set_threading_limits() + self.assertEqual(os.environ.get("MKL_NUM_THREADS"), "8") + self.assertEqual(os.environ.get("OMP_NUM_THREADS"), "8") + self.assertEqual(os.environ.get("OPENBLAS_NUM_THREADS"), "8") From 1832e6e3929787a0affcf5dc213c9befaef62401 Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 01:05:54 +0000 Subject: [PATCH 22/42] Update `CONTRIBUTING.md` and fix errors --- CONTRIBUTING.md | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6dd7f993..e6f67fe38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -241,10 +241,6 @@ to run it: check/mypy ``` -If your computer has multiple processor cores, you can add the option `-j 0` to the command above to -make Mypy run in parallel for a substantial speed increase. - - ### Linting and formatting Code should meet common style standards for Python and be free of error-prone constructs. We use @@ -290,14 +286,15 @@ tests, follow these general principles: messages. We use [pytest](https://docs.pytest.org) to run our tests and -[pytest-cov](https://pytest-cov.readthedocs.io) to compute coverage. +[pytest-cov](https://pytest-cov.readthedocs.io) to compute coverage. There are wrapper scripts +in the `check/` subdirectory to run these programs. -* While developing, periodically check that changes do not break anything. For fast checks, use - `pytest -m "not slow" PATH`, where `PATH` is a directory or pytest file to test. +* During development, periodically check that code changes do not break anything. For fast checks, + run `check/pytest -m "not slow" -n auto PATH ...`, where `PATH ...` is one or more directories + or `_test.py` files to test. (The `-n auto` option makes pytest use parallel processes; omit it + if it causes problems on your system.) -* After finishing a task, run `check/pytest` to test all of the OpenFermion code. If your system - has multiple processor cores, you can add the option `-n auto` to make pytest use multiple - parallel processes for a speed increase. (Beware, though, that this is resource-intensive.) +* After finishing a task, run all tests with `check/pytest -n auto`. We don't require 100% coverage, but coverage should be very high, and any uncovered code must be annotated with `# pragma: no cover`. To ignore coverage of a single line, place `# pragma: no cover` @@ -306,13 +303,12 @@ cover` comment on its own line. Note, however, that these annotations should be ### Final checks -After a task is finished, run each of the following to make sure everything passes all the tests: +After the work planned for a pull request is finished, run each of the following to make sure +everything passes all tests: -* `check/format-incremental` (and `check/format-incremental --apply` to auto-fix format problems) -* `check/pylint -j 0` -* `check/mypy` -* `check/pytest -n auto` -* `check/pytest-and-incremental-coverage` +```shell +check/all +``` ### Pull requests and code reviews From b51b431747e6163365541d10ad96ecf99db5812a Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 01:50:17 +0000 Subject: [PATCH 23/42] Don't pass --jobs=0 to pylint by default This was inconsistent with check/pylint. --- check/pylint-changed-files | 45 ++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/check/pylint-changed-files b/check/pylint-changed-files index 69500932a..c3248ad31 100755 --- a/check/pylint-changed-files +++ b/check/pylint-changed-files @@ -43,25 +43,36 @@ thisdir=$(dirname "${BASH_SOURCE[0]:?}") || exit $? repo_dir=$(git -C "${thisdir}" rev-parse --show-toplevel) || exit $? cd "${repo_dir}" || exit $? -# Figure out which revision to compare against. -if [ -n "$1" ] && [[ $1 != -* ]]; then - if ! git rev-parse --verify --quiet --no-revs "$1^{commit}"; then - echo -e "\033[31mNo revision '$1'.\033[0m" >&2 +# Figure out which revision to compare against and gather other arguments. +declare -a pylint_args=() +rev="" + +for arg in "$@"; do + if [[ "${arg}" == -* ]]; then + pylint_args+=( "${arg}" ) + elif [[ -z "${rev}" ]] && + git rev-parse --verify --quiet --no-revs "${arg}^{commit}" &> /dev/null; then + rev="${arg}" + else + pylint_args+=( "${arg}" ) + fi +done + +if [[ -z "${rev}" ]]; then + if [[ "$(git cat-file -t "upstream/main" 2> /dev/null)" == "commit" ]]; then + rev=upstream/main + elif [[ "$(git cat-file -t "origin/main" 2> /dev/null)" == "commit" ]]; then + rev=origin/main + elif [[ "$(git cat-file -t "main" 2> /dev/null)" == "commit" ]]; then + rev=main + else + echo -e "\033[31mNo default revision found to compare against. \ +Please specify a base revision (e.g. 'origin/main' or 'HEAD~1') as an argument.\033[0m" >&2 exit 1 fi - rev=$1 -elif [ "$(git cat-file -t "upstream/main" 2> /dev/null)" == "commit" ]; then - rev=upstream/main -elif [ "$(git cat-file -t "origin/main" 2> /dev/null)" == "commit" ]; then - rev=origin/main -elif [ "$(git cat-file -t "main" 2> /dev/null)" == "commit" ]; then - rev=main -else - echo -e "\033[31mNo default revision found to compare against. Argument #1 must be what to diff against (e.g. 'origin/main' or 'HEAD~1').\033[0m" >&2 - exit 1 fi base=$(git merge-base "${rev}" HEAD) -if [ "$(git rev-parse "${rev}")" == "${base}" ]; then +if [[ "$(git rev-parse "${rev}")" == "${base}" ]]; then echo -e "Comparing against revision '${rev}'." >&2 else echo -e "Comparing against revision '${rev}' (merge base ${base})." >&2 @@ -78,7 +89,7 @@ num_changed=${#changed[@]} # Run it. echo "Found ${num_changed} lintable files associated with changes." >&2 -if [ "${num_changed}" -eq 0 ]; then +if [[ "${num_changed}" -eq 0 ]]; then exit 0 fi -env PYTHONPATH=dev_tools pylint --jobs=0 "${changed[@]}" +env PYTHONPATH=dev_tools pylint "${pylint_args[@]}" "${changed[@]}" From 61c3b0a3597dd8833dab7de1de813962dcbbdc93 Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 02:04:17 +0000 Subject: [PATCH 24/42] Don't pass `-n auto` by default and update script --- check/pytest-and-incremental-coverage | 51 ++++++++++++++++----------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/check/pytest-and-incremental-coverage b/check/pytest-and-incremental-coverage index ed6c1e506..77c0a5e23 100755 --- a/check/pytest-and-incremental-coverage +++ b/check/pytest-and-incremental-coverage @@ -36,30 +36,39 @@ ################################################################################ # Get the working directory to the repo root. -cd "$( dirname "${BASH_SOURCE[0]}" )" || exit 1 +cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1 cd "$(git rev-parse --show-toplevel)" || exit 1 -# Figure out which revision to compare against. -if [[ -n "$1" && $1 != -* ]]; then - if [[ "$(git cat-file -t "$1" 2> /dev/null)" != "commit" ]]; then - echo -e "\033[31mNo revision '$1'.\033[0m" >&2 +# Figure out which revision to compare against and gather other arguments. +declare -a pytest_args=() +rev="" + +for arg in "$@"; do + if [[ "${arg}" == -* ]]; then + pytest_args+=( "${arg}" ) + elif [[ -z "${rev}" ]] && + git rev-parse --verify --quiet --no-revs "${arg}^{commit}" &> /dev/null; then + rev="${arg}" + else + pytest_args+=( "${arg}" ) + fi +done + +if [[ -z "${rev}" ]]; then + if [[ "$(git cat-file -t upstream/main 2> /dev/null)" == "commit" ]]; then + rev=upstream/main + elif [[ "$(git cat-file -t origin/main 2> /dev/null)" == "commit" ]]; then + rev=origin/main + elif [[ "$(git cat-file -t main 2> /dev/null)" == "commit" ]]; then + rev=main + else + echo -e "\033[31mNo default revision found to compare against. \ +Please specify a base revision (e.g. 'origin/main' or 'HEAD~1') as an argument.\033[0m" >&2 exit 1 fi - rev=$1 -elif [ "$(git cat-file -t upstream/main 2> /dev/null)" == "commit" ]; then - rev=upstream/main -elif [ "$(git cat-file -t origin/main 2> /dev/null)" == "commit" ]; then - rev=origin/main -elif [ "$(git cat-file -t main 2> /dev/null)" == "commit" ]; then - rev=main -else - echo -e "\033[31mNo default revision found to compare against. Argument #1 must be what to diff against (e.g. 'origin/main' or 'HEAD~1').\033[0m" >&2 - exit 1 fi -# shellcheck disable=SC2086 -base="$(git merge-base ${rev} HEAD)" -# shellcheck disable=SC2086 -if [ "$(git rev-parse ${rev})" == "${base}" ]; then +base="$(git merge-base "${rev}" HEAD)" +if [[ "$(git rev-parse "${rev}")" == "${base}" ]]; then echo -e "Comparing against revision '${rev}'." >&2 else echo -e "Comparing against revision '${rev}' (merge base ${base})." >&2 @@ -68,7 +77,7 @@ fi # Run tests while producing coverage files. check/pytest . \ - -n auto \ + "${pytest_args[@]}" \ --actually-quiet \ --cov \ --cov-report=annotate @@ -82,7 +91,7 @@ cover_result=$? find . | grep "\.py,cover$" | xargs rm -f # Report result. -if [ "${pytest_result}" -ne "0" ] || [ "${cover_result}" -ne "0" ]; then +if [[ "${pytest_result}" -ne "0" ]] || [[ "${cover_result}" -ne "0" ]]; then exit 1 fi exit 0 From aa6d6373374cf354df200e16362fee42497948e6 Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 02:58:52 +0000 Subject: [PATCH 25/42] Set the thread variables unconditionally --- src/openfermion/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/openfermion/config.py b/src/openfermion/config.py index 781b52bed..5347af088 100644 --- a/src/openfermion/config.py +++ b/src/openfermion/config.py @@ -66,5 +66,4 @@ def set_threading_limits() -> None: else: limit = str(max(get_available_cpu_count() - 1, 1)) for var in ["MKL_NUM_THREADS", "OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS"]: - if var not in os.environ: - os.environ[var] = limit + os.environ[var] = limit From 02c43920d28a6cd233abd31215b8ca199cab5f57 Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 03:05:43 +0000 Subject: [PATCH 26/42] Take out conditional setting of the threads variables --- src/openfermion/config_test.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/openfermion/config_test.py b/src/openfermion/config_test.py index 0158ab8e6..303681343 100644 --- a/src/openfermion/config_test.py +++ b/src/openfermion/config_test.py @@ -155,16 +155,7 @@ def test_set_threading_limits(self): self.assertEqual(os.environ.get("OMP_NUM_THREADS"), "7") self.assertEqual(os.environ.get("OPENBLAS_NUM_THREADS"), "7") - # Test respecting existing values. - env = {"OMP_NUM_THREADS": "3"} - with patch.dict(os.environ, env, clear=True): - with patch("openfermion.config.get_available_cpu_count", return_value=8): - set_threading_limits() - self.assertEqual(os.environ.get("MKL_NUM_THREADS"), "7") - self.assertEqual(os.environ.get("OMP_NUM_THREADS"), "3") # Respected! - self.assertEqual(os.environ.get("OPENBLAS_NUM_THREADS"), "7") - - # Test when PYTEST_XDIST_WORKER_COUNT is in os.environ (line 65 coverage). + # Test when PYTEST_XDIST_WORKER_COUNT is in os.environ. env = {"PYTEST_XDIST_WORKER_COUNT": "4"} with patch.dict(os.environ, env, clear=True): with patch("openfermion.config.get_available_cpu_count", return_value=8): @@ -172,3 +163,10 @@ def test_set_threading_limits(self): self.assertEqual(os.environ.get("MKL_NUM_THREADS"), "8") self.assertEqual(os.environ.get("OMP_NUM_THREADS"), "8") self.assertEqual(os.environ.get("OPENBLAS_NUM_THREADS"), "8") + + # Test when there are existing values. + env = {"OMP_NUM_THREADS": "3"} + with patch.dict(os.environ, env, clear=True): + with patch("openfermion.config.get_available_cpu_count", return_value=8): + set_threading_limits() + self.assertEqual(os.environ.get("OMP_NUM_THREADS"), "7") From 3918f93a61aba90fbc76dc90ffecad8f1ad3ae1f Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 03:15:13 +0000 Subject: [PATCH 27/42] Change handling of parallelism in check/all --- check/all | 80 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/check/all b/check/all index 1a09b79cc..1111a4d2a 100755 --- a/check/all +++ b/check/all @@ -17,22 +17,34 @@ # This script is modeled in part on Cirq's check/all script. declare -r usage="\ -Usage: ${0##*/} [--help] [--only-changed-files] [--apply-format-changes] [BASE_REV] +Usage: ${0##*/} [OPTIONS] [BASE_REV] -If the first argument given on the command line is the option --help or -h, -this program prints usage information and then exits. - -With no arguments, this runs multiple checks on the code base. These tests -are based on the programs in the checks/ subdirectory. +Runs multiple checks on the code base. -If --apply-format-changes is specified, the flag --apply will be passed to -check/format-incremental to apply the format changes suggested by the +If --apply-changes (or -a) is given, the flag --apply will be passed to +check/format-incremental to make it apply format changes suggested by the formatter. -You can specify a base git revision to compare against (i.e., to use when -determining whether or not a file is considered to have changed). If given, -the argument BASE_REV is passed on to tests that can use it, such as -check/pytest-and-incremental-coverage." +If --only-changed-files (or -c) is given, most checks will be limited to the +files that have changed since the BASE_REV version. If BASE_REV is not given, +the version at the end of the base branch (origin/main) will be used. + +By default, this program runs checks in parallel mode. To prevent this, use the +option --no-parallel (or -p). + +You can specify a base git revision to compare against (that is, the revision +to be used when determining whether a file is considered to have changed). If +given, the argument BASE_REV is passed on to tests that can use it, such as +check/pytest-and-incremental-coverage. + +If the first argument given on the command line is the option --help or -h, +this program prints usage information and then exits. + +Summary of available options: + --apply-changes Pass the flag --apply to check/format-incremental + --no-parallel Do not pass the parallelism flags to the check programs + --only-changed-files Limit checks to files that have changed since BASE_REV + -h or --help Show this help message and exit" set -eo pipefail -o errtrace shopt -s inherit_errexit @@ -51,53 +63,63 @@ function error() { declare -a rev=() declare -a apply_arg=() declare only_changed="" -for arg in "$@"; do - case "${arg}" in +declare no_parallel="" + +while [[ $# -gt 0 ]]; do + case "$1" in + -a | --apply-changes | --apply-format-changes) + apply_arg=( "--apply" ) + ;; + -c | --only-changed-files) + only_changed="true" + ;; -h | --help) echo "${usage}" exit 0 ;; - --apply-format-changes) - apply_arg=( "--apply" ) - shift - ;; - --only-changed-files) - only_changed="true" - shift + -p | --no-parallel) + no_parallel="true" ;; -*) - error "Invalid option '${arg}'" + error "Invalid option '$1'" error "See '$0 --help' for the list of supported options." exit 1 ;; *) - if ! rev=( "$(git rev-parse --verify --end-of-options "${arg}^{commit}")" ); then - error "No revision '${arg}'" + if ! rev=( "$(git rev-parse --verify --end-of-options "$1^{commit}")" ); then + error "No revision '$1'" exit 1 fi ;; esac + shift done # ~~~~ Run the tests ~~~~ declare -a errors=() +declare -a pylint_j=() +declare -a pytest_n=() function run() { - echo "Running $* ..." + echo "Running $*" "$@" || errors+=( "$* failed" ) - echo } +if [[ -z "${no_parallel}" ]]; then + pylint_j=("-j0") + pytest_n=("-n" "auto") +fi + if [[ -n "${only_changed}" ]]; then run check/format-incremental "${rev[@]}" "${apply_arg[@]}" - run check/pylint-changed-files "${rev[@]}" + run check/pylint-changed-files "${pylint_j[@]}" "${rev[@]}" else run check/format-incremental "${rev[@]}" "${apply_arg[@]}" --all - run check/pylint "${rev[@]}" + run check/pylint "${pylint_j[@]}" "${rev[@]}" fi run check/mypy -run check/pytest-and-incremental-coverage "${rev[@]}" +run check/pytest-and-incremental-coverage "${pytest_n[@]}" "${rev[@]}" run check/shellcheck run check/nbformat From 79146487be07e01b3b4b68243522d63097005273 Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 03:52:52 +0000 Subject: [PATCH 28/42] Add threadpoolctl to the pytest dependencies --- dev_tools/requirements/deps/pytest.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev_tools/requirements/deps/pytest.txt b/dev_tools/requirements/deps/pytest.txt index 0dca16a9f..c56b085c8 100644 --- a/dev_tools/requirements/deps/pytest.txt +++ b/dev_tools/requirements/deps/pytest.txt @@ -7,3 +7,4 @@ pytest-xdist nbformat tensorflow-docs +threadpoolctl From db7309b5b144aaa956fe5f9414cd2996563c21d3 Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 03:56:05 +0000 Subject: [PATCH 29/42] Use different number of items for affinity test Adjusted a test per review comment from Adam R. --- src/openfermion/config_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openfermion/config_test.py b/src/openfermion/config_test.py index 303681343..c0fd7f79f 100644 --- a/src/openfermion/config_test.py +++ b/src/openfermion/config_test.py @@ -55,9 +55,9 @@ def test_get_available_cpu_count_various_os(self): self.assertEqual(get_available_cpu_count(), 1) # Test Case 2: Linux/Unix (no process_cpu_count, has sched_getaffinity). - mock_os = MockOS(sched_getaffinity_val=[1, 2, 3, 4]) + mock_os = MockOS(sched_getaffinity_val=[1, 2, 3]) with patch.object(config, "os", mock_os): - self.assertEqual(get_available_cpu_count(), 4) + self.assertEqual(get_available_cpu_count(), 3) # Test Case 3: Linux/Unix but sched_getaffinity raises exception. mock_os = MockOS(sched_getaffinity_val=ValueError("error"), cpu_count_val=4) From e6fa64fa5f49e2a762a522196177a4ad9fe1dc1f Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 03:58:26 +0000 Subject: [PATCH 30/42] Regenerate requirements files --- dev_tools/requirements/envs/dev.env.txt | 4 + dev_tools/requirements/envs/pylint.env.txt | 6 + .../requirements/envs/pytest-extra.env.txt | 6 + dev_tools/requirements/envs/pytest.env.txt | 6 + .../max_compat/pytest-max-compat.env.txt | 206 +++++------------- 5 files changed, 72 insertions(+), 156 deletions(-) diff --git a/dev_tools/requirements/envs/dev.env.txt b/dev_tools/requirements/envs/dev.env.txt index f1c4bc2ad..b6abff1a0 100644 --- a/dev_tools/requirements/envs/dev.env.txt +++ b/dev_tools/requirements/envs/dev.env.txt @@ -2018,6 +2018,10 @@ sympy==1.14.0 \ tensorflow-docs==2025.2.19.33219 \ --hash=sha256:b52682f3739110ce804af701f9ab5ac1efe07760109a5b5640769851d7d14b19 # via -r dev_tools/requirements/deps/pytest.txt +threadpoolctl==3.6.0 \ + --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ + --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e + # via -r dev_tools/requirements/deps/pytest.txt tomli==2.4.1 ; python_full_version <= '3.11' \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ diff --git a/dev_tools/requirements/envs/pylint.env.txt b/dev_tools/requirements/envs/pylint.env.txt index 5868211c4..906531b76 100644 --- a/dev_tools/requirements/envs/pylint.env.txt +++ b/dev_tools/requirements/envs/pylint.env.txt @@ -1578,6 +1578,12 @@ tensorflow-docs==2025.2.19.33219 \ # via # -c dev_tools/requirements/envs/dev.env.txt # -r dev_tools/requirements/deps/pytest.txt +threadpoolctl==3.6.0 \ + --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ + --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e + # via + # -c dev_tools/requirements/envs/dev.env.txt + # -r dev_tools/requirements/deps/pytest.txt tomli==2.4.1 ; python_full_version <= '3.11' \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ diff --git a/dev_tools/requirements/envs/pytest-extra.env.txt b/dev_tools/requirements/envs/pytest-extra.env.txt index acce1f0dd..147da67ee 100644 --- a/dev_tools/requirements/envs/pytest-extra.env.txt +++ b/dev_tools/requirements/envs/pytest-extra.env.txt @@ -1546,6 +1546,12 @@ tensorflow-docs==2025.2.19.33219 \ # via # -c dev_tools/requirements/envs/dev.env.txt # -r dev_tools/requirements/deps/pytest.txt +threadpoolctl==3.6.0 \ + --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ + --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e + # via + # -c dev_tools/requirements/envs/dev.env.txt + # -r dev_tools/requirements/deps/pytest.txt tomli==2.4.1 ; python_full_version <= '3.11' \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ diff --git a/dev_tools/requirements/envs/pytest.env.txt b/dev_tools/requirements/envs/pytest.env.txt index afad91589..b116bc55b 100644 --- a/dev_tools/requirements/envs/pytest.env.txt +++ b/dev_tools/requirements/envs/pytest.env.txt @@ -1433,6 +1433,12 @@ tensorflow-docs==2025.2.19.33219 \ # via # -c dev_tools/requirements/envs/dev.env.txt # -r dev_tools/requirements/deps/pytest.txt +threadpoolctl==3.6.0 \ + --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ + --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e + # via + # -c dev_tools/requirements/envs/dev.env.txt + # -r dev_tools/requirements/deps/pytest.txt tomli==2.4.1 ; python_full_version <= '3.11' \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ diff --git a/dev_tools/requirements/max_compat/pytest-max-compat.env.txt b/dev_tools/requirements/max_compat/pytest-max-compat.env.txt index 30789a0db..082214d2c 100644 --- a/dev_tools/requirements/max_compat/pytest-max-compat.env.txt +++ b/dev_tools/requirements/max_compat/pytest-max-compat.env.txt @@ -3,35 +3,26 @@ absl-py==2.4.0 \ --hash=sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d \ --hash=sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # tensorflow-docs + # via tensorflow-docs astor==0.8.1 \ --hash=sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5 \ --hash=sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # tensorflow-docs + # via tensorflow-docs attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # cirq-core # jsonschema # referencing backports-asyncio-runner==1.2.0 ; python_full_version < '3.11' \ --hash=sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5 \ --hash=sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # pytest-asyncio + # via pytest-asyncio certifi==2026.5.20 \ --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \ --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # requests + # via requests charset-normalizer==3.4.7 \ --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ @@ -162,26 +153,21 @@ charset-normalizer==3.4.7 \ --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # requests + # via requests cirq-core==1.4.1 ; python_full_version < '3.11' \ --hash=sha256:869db60413265c41a8206854c1d4ca9bad5fac9cfd7c6a10685b5a6d516defa0 # via # -c dev_tools/requirements/deps/oldest-versions.txt - # -c dev_tools/requirements/max_compat/dev.env.txt # -r dev_tools/requirements/deps/runtime.txt cirq-core==1.6.1 ; python_full_version >= '3.11' \ --hash=sha256:fbc809d7c6a228762ad92bb7870dc16f55b76728fcc7ef4c7aaceb7e5c63e8e6 # via # -c dev_tools/requirements/deps/oldest-versions.txt - # -c dev_tools/requirements/max_compat/dev.env.txt # -r dev_tools/requirements/deps/runtime.txt colorama==0.4.6 ; sys_platform == 'win32' \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # pytest # tqdm contourpy==1.3.2 \ @@ -242,9 +228,7 @@ contourpy==1.3.2 \ --hash=sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe \ --hash=sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0 \ --hash=sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # matplotlib + # via matplotlib coverage==7.14.1 \ --hash=sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86 \ --hash=sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd \ @@ -352,45 +336,31 @@ coverage==7.14.1 \ --hash=sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c \ --hash=sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253 \ --hash=sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # pytest-cov + # via pytest-cov cycler==0.12.1 \ --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # matplotlib + # via matplotlib deprecation==2.1.0 \ --hash=sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff \ --hash=sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # -r dev_tools/requirements/deps/runtime.txt + # via -r dev_tools/requirements/deps/runtime.txt duet==0.2.9 \ --hash=sha256:a16088b68b0faee8aee12cdf4d0a8af060ed958badb44f3e32f123f13f64119a \ --hash=sha256:d6fa39582e6a3dce1096c47e5fbcbda648a633eed94a38943e68662afa2587f3 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # cirq-core + # via cirq-core exceptiongroup==1.3.1 ; python_full_version < '3.11' \ --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # pytest + # via pytest execnet==2.1.2 \ --hash=sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd \ --hash=sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # pytest-xdist + # via pytest-xdist fastjsonschema==2.21.2 \ --hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \ --hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # nbformat + # via nbformat fonttools==4.63.0 \ --hash=sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69 \ --hash=sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c \ @@ -442,9 +412,7 @@ fonttools==4.63.0 \ --hash=sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b \ --hash=sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af \ --hash=sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # matplotlib + # via matplotlib h5py==3.16.0 \ --hash=sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524 \ --hash=sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242 \ @@ -494,45 +462,31 @@ h5py==3.16.0 \ --hash=sha256:fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3 \ --hash=sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527 \ --hash=sha256:ffbab2fedd6581f6aa31cf1639ca2cb86e02779de525667892ebf4cc9fd26434 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # -r dev_tools/requirements/deps/runtime.txt + # via -r dev_tools/requirements/deps/runtime.txt idna==3.17 \ --hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \ --hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # requests + # via requests iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # pytest + # via pytest jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # tensorflow-docs + # via tensorflow-docs jsonschema==4.26.0 \ --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # nbformat + # via nbformat jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # jsonschema + # via jsonschema jupyter-core==5.9.1 \ --hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \ --hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # nbformat + # via nbformat kiwisolver==1.5.0 \ --hash=sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9 \ --hash=sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679 \ @@ -651,9 +605,7 @@ kiwisolver==1.5.0 \ --hash=sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3 \ --hash=sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09 \ --hash=sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # matplotlib + # via matplotlib markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ @@ -744,9 +696,7 @@ markupsafe==3.0.3 \ --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # jinja2 + # via jinja2 matplotlib==3.10.9 \ --hash=sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9 \ --hash=sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42 \ @@ -803,27 +753,21 @@ matplotlib==3.10.9 \ --hash=sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921 \ --hash=sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba \ --hash=sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # cirq-core + # via cirq-core mpmath==1.3.0 \ --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # sympy + # via sympy nbformat==5.10.4 \ --hash=sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a \ --hash=sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b # via - # -c dev_tools/requirements/max_compat/dev.env.txt # -r dev_tools/requirements/deps/pytest.txt # tensorflow-docs networkx==3.4.2 \ --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \ --hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f # via - # -c dev_tools/requirements/max_compat/dev.env.txt # -r dev_tools/requirements/deps/runtime.txt # cirq-core numpy==1.26.4 \ @@ -864,7 +808,6 @@ numpy==1.26.4 \ --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f # via - # -c dev_tools/requirements/max_compat/dev.env.txt # -r dev_tools/requirements/deps/runtime.txt # cirq-core # contourpy @@ -876,7 +819,6 @@ packaging==26.2 \ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # deprecation # matplotlib # pytest @@ -936,9 +878,7 @@ pandas==2.3.3 \ --hash=sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # cirq-core + # via cirq-core pillow==12.2.0 \ --hash=sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9 \ --hash=sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5 \ @@ -1031,20 +971,15 @@ pillow==12.2.0 \ --hash=sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7 \ --hash=sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06 \ --hash=sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # matplotlib + # via matplotlib platformdirs==4.10.0 \ --hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \ --hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # jupyter-core + # via jupyter-core pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # pytest # pytest-cov protobuf==7.35.0 \ @@ -1056,32 +991,23 @@ protobuf==7.35.0 \ --hash=sha256:c13f325cf242bad135c350629eeb5d54b24228eb472fb3e2e9ebbd4c5dc20ca0 \ --hash=sha256:f05bcadf9a2a6b8dda047007075135fb7d08c73d9177aabc067e1be46881a201 \ --hash=sha256:fcbe42a4ac09d3ec9c987ddfcd956afd0b15f1ff613bd8371bde9405ffd5c8e5 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # tensorflow-docs + # via tensorflow-docs pubchempy==1.0.5 \ --hash=sha256:08f0b2a82a5caa5d61e14935d655da554602d7b5686fe661ab584c882ffff623 \ --hash=sha256:e936cfed31fa194042ad463be3c803dde5b12ef2f795caf336e3114127c34fa0 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # -r dev_tools/requirements/deps/runtime.txt + # via -r dev_tools/requirements/deps/runtime.txt pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # pytest + # via pytest pyparsing==3.3.2 \ --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # matplotlib + # via matplotlib pytest==9.0.3 \ --hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \ --hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c # via - # -c dev_tools/requirements/max_compat/dev.env.txt # -r dev_tools/requirements/deps/pytest.txt # pytest-asyncio # pytest-cov @@ -1091,46 +1017,33 @@ pytest==9.0.3 \ pytest-asyncio==1.4.0 \ --hash=sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1 \ --hash=sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # -r dev_tools/requirements/deps/pytest.txt + # via -r dev_tools/requirements/deps/pytest.txt pytest-cov==7.1.0 \ --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # -r dev_tools/requirements/deps/pytest.txt + # via -r dev_tools/requirements/deps/pytest.txt pytest-randomly==4.1.0 \ --hash=sha256:47f1d9746c3bc3efabd53ae1ebfb8bb385cf3d4df4b505b6d58d9c97a3dfe70f \ --hash=sha256:f55e89e53367b090c0c053697d7f9d77595543d0e0516c93978b50c0f6b252f9 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # -r dev_tools/requirements/deps/pytest.txt + # via -r dev_tools/requirements/deps/pytest.txt pytest-retry==1.7.0 \ --hash=sha256:a2dac85b79a4e2375943f1429479c65beb6c69553e7dae6b8332be47a60954f4 \ --hash=sha256:f8d52339f01e949df47c11ba9ee8d5b362f5824dff580d3870ec9ae0057df80f - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # -r dev_tools/requirements/deps/pytest.txt + # via -r dev_tools/requirements/deps/pytest.txt pytest-xdist==3.8.0 \ --hash=sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88 \ --hash=sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # -r dev_tools/requirements/deps/pytest.txt + # via -r dev_tools/requirements/deps/pytest.txt python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # matplotlib # pandas pytz==2026.2 \ --hash=sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126 \ --hash=sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # pandas + # via pandas pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ @@ -1205,22 +1118,17 @@ pyyaml==6.0.3 \ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # tensorflow-docs + # via tensorflow-docs referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # jsonschema # jsonschema-specifications requests==2.34.2 \ --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # -r dev_tools/requirements/deps/runtime.txt + # via -r dev_tools/requirements/deps/runtime.txt rpds-py==0.30.0 \ --hash=sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f \ --hash=sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136 \ @@ -1338,7 +1246,6 @@ rpds-py==0.30.0 \ --hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f \ --hash=sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # jsonschema # referencing scipy==1.15.3 \ @@ -1389,33 +1296,29 @@ scipy==1.15.3 \ --hash=sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e \ --hash=sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # -r dev_tools/requirements/deps/runtime.txt # cirq-core six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # python-dateutil + # via python-dateutil sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # cirq-core + # via cirq-core sympy==1.14.0 \ --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # -r dev_tools/requirements/deps/runtime.txt # cirq-core tensorflow-docs==2025.2.19.33219 \ --hash=sha256:b52682f3739110ce804af701f9ab5ac1efe07760109a5b5640769851d7d14b19 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # -r dev_tools/requirements/deps/pytest.txt + # via -r dev_tools/requirements/deps/pytest.txt +threadpoolctl==3.6.0 \ + --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ + --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e + # via -r dev_tools/requirements/deps/pytest.txt tomli==2.4.1 ; python_full_version <= '3.11' \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ @@ -1465,27 +1368,22 @@ tomli==2.4.1 ; python_full_version <= '3.11' \ --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # coverage # pytest tqdm==4.67.3 \ --hash=sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb \ --hash=sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # cirq-core + # via cirq-core traitlets==5.15.0 \ --hash=sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971 \ --hash=sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # jupyter-core # nbformat typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via - # -c dev_tools/requirements/max_compat/dev.env.txt # cirq-core # exceptiongroup # pytest-asyncio @@ -1493,12 +1391,8 @@ typing-extensions==4.15.0 \ tzdata==2026.2 \ --hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \ --hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # pandas + # via pandas urllib3==2.7.0 \ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 - # via - # -c dev_tools/requirements/max_compat/dev.env.txt - # requests + # via requests From 43ec834cbad36cd787c9717b395be40ee7ddfaae Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 04:04:18 +0000 Subject: [PATCH 31/42] Use self.qubit_operator Per review comment by Adam. --- src/openfermion/linalg/linear_qubit_operator_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/openfermion/linalg/linear_qubit_operator_test.py b/src/openfermion/linalg/linear_qubit_operator_test.py index cd9f7d005..898b85746 100644 --- a/src/openfermion/linalg/linear_qubit_operator_test.py +++ b/src/openfermion/linalg/linear_qubit_operator_test.py @@ -264,10 +264,9 @@ def test_closed_workers_not_reused(self): def test_matvec_single_process(self): """Tests that when processes is 1, it computes correctly and doesn't crash.""" - qubit_operator = QubitOperator('Z3') + QubitOperator('Y0') + QubitOperator('X1') options = LinearQubitOperatorOptions(processes=1) parallel_qubit_op = ParallelLinearQubitOperator( - qubit_operator, self.n_qubits, options=options + self.qubit_operator, self.n_qubits, options=options ) self.assertTrue(numpy.allclose(parallel_qubit_op * self.vec, self.expected_matvec)) From b49816ceafe8b9b52ea4978b64d388107ae56532 Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 17:39:05 +0000 Subject: [PATCH 32/42] Be more DRY --- check/pylint-changed-files | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/check/pylint-changed-files b/check/pylint-changed-files index c3248ad31..75c83bfdf 100755 --- a/check/pylint-changed-files +++ b/check/pylint-changed-files @@ -59,18 +59,19 @@ for arg in "$@"; do done if [[ -z "${rev}" ]]; then - if [[ "$(git cat-file -t "upstream/main" 2> /dev/null)" == "commit" ]]; then - rev=upstream/main - elif [[ "$(git cat-file -t "origin/main" 2> /dev/null)" == "commit" ]]; then - rev=origin/main - elif [[ "$(git cat-file -t "main" 2> /dev/null)" == "commit" ]]; then - rev=main - else + for candidate in upstream/main origin/main main; do + if [[ "$(git cat-file -t "${candidate}" 2>/dev/null)" == "commit" ]]; then + rev="${candidate}" + break + fi + done + if [[ -z "${rev}" ]]; then echo -e "\033[31mNo default revision found to compare against. \ Please specify a base revision (e.g. 'origin/main' or 'HEAD~1') as an argument.\033[0m" >&2 exit 1 fi fi + base=$(git merge-base "${rev}" HEAD) if [[ "$(git rev-parse "${rev}")" == "${base}" ]]; then echo -e "Comparing against revision '${rev}'." >&2 From 09ec54422fbac2b5a96eaa3f50de207167fe4acd Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 17:41:19 +0000 Subject: [PATCH 33/42] Be more DRY --- check/pytest-and-incremental-coverage | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/check/pytest-and-incremental-coverage b/check/pytest-and-incremental-coverage index 77c0a5e23..a716ffa82 100755 --- a/check/pytest-and-incremental-coverage +++ b/check/pytest-and-incremental-coverage @@ -55,18 +55,19 @@ for arg in "$@"; do done if [[ -z "${rev}" ]]; then - if [[ "$(git cat-file -t upstream/main 2> /dev/null)" == "commit" ]]; then - rev=upstream/main - elif [[ "$(git cat-file -t origin/main 2> /dev/null)" == "commit" ]]; then - rev=origin/main - elif [[ "$(git cat-file -t main 2> /dev/null)" == "commit" ]]; then - rev=main - else + for candidate in upstream/main origin/main main; do + if [[ "$(git cat-file -t "${candidate}" 2>/dev/null)" == "commit" ]]; then + rev="${candidate}" + break + fi + done + if [[ -z "${rev}" ]]; then echo -e "\033[31mNo default revision found to compare against. \ Please specify a base revision (e.g. 'origin/main' or 'HEAD~1') as an argument.\033[0m" >&2 exit 1 fi fi + base="$(git merge-base "${rev}" HEAD)" if [[ "$(git rev-parse "${rev}")" == "${base}" ]]; then echo -e "Comparing against revision '${rev}'." >&2 From e682c5ef943bd04a5d480e4d3207506ae51c6b80 Mon Sep 17 00:00:00 2001 From: mhucka Date: Fri, 10 Jul 2026 17:45:38 +0000 Subject: [PATCH 34/42] Fix typo --- conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 31caabe2c..080bcba9d 100644 --- a/conftest.py +++ b/conftest.py @@ -49,7 +49,7 @@ def set_threadpool_limits(): """Limit number of threads to prevent oversubscription with pytest-xdist. This only has an effect if the Python threadpoolctl package is installed, - and it only influences parallellism in some numerical libraries used in + and it only influences parallelism in some numerical libraries used in packages such as NumPy. """ try: From e1c0cbc3197ae2199b3fdd0e697dbb0b72aea494 Mon Sep 17 00:00:00 2001 From: mhucka Date: Sat, 11 Jul 2026 02:14:43 +0000 Subject: [PATCH 35/42] Do not set pylint "jobs" in `.pylintrc` --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index a77a7e2b0..39450d38f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -90,7 +90,7 @@ ignored-modules= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use, and will cap the count on Windows to # avoid hangs. -jobs=0 +# jobs=0 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or From 2cb9c1423fd2083801f8f28329bf60996be62a2e Mon Sep 17 00:00:00 2001 From: mhucka Date: Sat, 11 Jul 2026 02:54:05 +0000 Subject: [PATCH 36/42] Make check/pylint print something when it's successful --- check/pylint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check/pylint b/check/pylint index 9239f8c1d..16c2f6d07 100755 --- a/check/pylint +++ b/check/pylint @@ -24,4 +24,4 @@ cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" || exit 1 cd "$(git rev-parse --show-toplevel)" || exit 1 -pylint "$@" src dev_tools +pylint "$@" src dev_tools && echo "Pylint completed successfully." From 74d65ceecb386774d4adca571e566d2347ea1224 Mon Sep 17 00:00:00 2001 From: mhucka Date: Sat, 11 Jul 2026 02:55:19 +0000 Subject: [PATCH 37/42] Add psutil to pytest dependencies --- dev_tools/requirements/deps/pytest.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev_tools/requirements/deps/pytest.txt b/dev_tools/requirements/deps/pytest.txt index c56b085c8..552dcefc0 100644 --- a/dev_tools/requirements/deps/pytest.txt +++ b/dev_tools/requirements/deps/pytest.txt @@ -6,5 +6,6 @@ pytest-retry pytest-xdist nbformat +psutil tensorflow-docs threadpoolctl From a9bbf02fb41bc4f33e0494d43e862b131a16c099 Mon Sep 17 00:00:00 2001 From: mhucka Date: Sat, 11 Jul 2026 02:56:26 +0000 Subject: [PATCH 38/42] Add parallelism option to mypy --- check/all | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/check/all b/check/all index 1111a4d2a..bc1341613 100755 --- a/check/all +++ b/check/all @@ -100,15 +100,23 @@ done declare -a errors=() declare -a pylint_j=() declare -a pytest_n=() +declare -a mypy_n=() function run() { echo "Running $*" "$@" || errors+=( "$* failed" ) } +# Count only the physical cores (usually fewer than logical cores). This leaves +# some CPU resources for OpenFermion modules that use multiple processes. +cpus=$(python3 -c 'import psutil; print(psutil.cpu_count(logical=False))') + if [[ -z "${no_parallel}" ]]; then - pylint_j=("-j0") - pytest_n=("-n" "auto") + # Pylint & pytest can auto-detect the number of CPUs, but not mypy. To be + # consistent, this uses the same explicit number for all of them. + pylint_j=("-j" "${cpus}") + pytest_n=("-n" "${cpus}") + mypy_n=("-n" "${cpus}") fi if [[ -n "${only_changed}" ]]; then @@ -118,7 +126,7 @@ else run check/format-incremental "${rev[@]}" "${apply_arg[@]}" --all run check/pylint "${pylint_j[@]}" "${rev[@]}" fi -run check/mypy +run check/mypy "${mypy_n[@]}" run check/pytest-and-incremental-coverage "${pytest_n[@]}" "${rev[@]}" run check/shellcheck run check/nbformat From f329f9f2c655c23ce55bdc4bdbb1c8811b1b9cc0 Mon Sep 17 00:00:00 2001 From: mhucka Date: Sat, 11 Jul 2026 02:56:48 +0000 Subject: [PATCH 39/42] Print a more noticeable "running ABC" message --- check/all | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check/all b/check/all index bc1341613..04946a011 100755 --- a/check/all +++ b/check/all @@ -103,7 +103,7 @@ declare -a pytest_n=() declare -a mypy_n=() function run() { - echo "Running $*" + echo "~~~~ Running $* ~~~~" "$@" || errors+=( "$* failed" ) } From a6fd00e446d1265a9a4ab06ee20a6d31c90cc380 Mon Sep 17 00:00:00 2001 From: mhucka Date: Sat, 11 Jul 2026 02:58:40 +0000 Subject: [PATCH 40/42] Make check/shellcheck print something when it's successful --- check/shellcheck | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/check/shellcheck b/check/shellcheck index 1c796506c..85be18a98 100755 --- a/check/shellcheck +++ b/check/shellcheck @@ -81,5 +81,6 @@ if (( opt_dry_run )); then printf '\\\n %s ' "${our_shell_scripts[@]}" printf '\\\n;\n' else - shellcheck "${shellcheck_options[@]}" "${our_shell_scripts[@]}" + shellcheck "${shellcheck_options[@]}" "${our_shell_scripts[@]}" && \ + echo "shellcheck completed successfully." fi From 8e50d3ca71b3e3236543f6c66c46155647c691c4 Mon Sep 17 00:00:00 2001 From: mhucka Date: Sat, 11 Jul 2026 03:01:09 +0000 Subject: [PATCH 41/42] Tell people about the `-j 0` option --- CONTRIBUTING.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6f67fe38..e4b603e49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -263,9 +263,12 @@ for formatting code, and provide scripts to run them. * To run the linter: ```shell - check/pylint-changed-files + check/pylint-changed-files -j 0 ``` + (The option `-j 0` tells pylint to use parallel processes; omit it if it causes problems on your + system.) + ### Testing and test coverage When new functions, classes, and files are introduced, they should also have corresponding tests. From 7f36f75934de08c77eda361f6b5bf9430aa0dc61 Mon Sep 17 00:00:00 2001 From: mhucka Date: Sat, 11 Jul 2026 03:30:08 +0000 Subject: [PATCH 42/42] Regenerate requirements files after change to pytest.txt --- dev_tools/requirements/envs/dev.env.txt | 23 +++++++++++++++++ dev_tools/requirements/envs/pylint.env.txt | 25 +++++++++++++++++++ .../requirements/envs/pytest-extra.env.txt | 25 +++++++++++++++++++ dev_tools/requirements/envs/pytest.env.txt | 25 +++++++++++++++++++ .../max_compat/pytest-max-compat.env.txt | 23 +++++++++++++++++ 5 files changed, 121 insertions(+) diff --git a/dev_tools/requirements/envs/dev.env.txt b/dev_tools/requirements/envs/dev.env.txt index b6abff1a0..f8dabf30e 100644 --- a/dev_tools/requirements/envs/dev.env.txt +++ b/dev_tools/requirements/envs/dev.env.txt @@ -1581,6 +1581,29 @@ protobuf==7.35.0 \ --hash=sha256:f05bcadf9a2a6b8dda047007075135fb7d08c73d9177aabc067e1be46881a201 \ --hash=sha256:fcbe42a4ac09d3ec9c987ddfcd956afd0b15f1ff613bd8371bde9405ffd5c8e5 # via tensorflow-docs +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via -r dev_tools/requirements/deps/pytest.txt pubchempy==1.0.5 \ --hash=sha256:08f0b2a82a5caa5d61e14935d655da554602d7b5686fe661ab584c882ffff623 \ --hash=sha256:e936cfed31fa194042ad463be3c803dde5b12ef2f795caf336e3114127c34fa0 diff --git a/dev_tools/requirements/envs/pylint.env.txt b/dev_tools/requirements/envs/pylint.env.txt index 906531b76..82a7bd938 100644 --- a/dev_tools/requirements/envs/pylint.env.txt +++ b/dev_tools/requirements/envs/pylint.env.txt @@ -1196,6 +1196,31 @@ protobuf==7.35.0 \ # via # -c dev_tools/requirements/envs/dev.env.txt # tensorflow-docs +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via + # -c dev_tools/requirements/envs/dev.env.txt + # -r dev_tools/requirements/deps/pytest.txt pubchempy==1.0.5 \ --hash=sha256:08f0b2a82a5caa5d61e14935d655da554602d7b5686fe661ab584c882ffff623 \ --hash=sha256:e936cfed31fa194042ad463be3c803dde5b12ef2f795caf336e3114127c34fa0 diff --git a/dev_tools/requirements/envs/pytest-extra.env.txt b/dev_tools/requirements/envs/pytest-extra.env.txt index 147da67ee..e6568c5fb 100644 --- a/dev_tools/requirements/envs/pytest-extra.env.txt +++ b/dev_tools/requirements/envs/pytest-extra.env.txt @@ -1170,6 +1170,31 @@ protobuf==7.35.0 \ # via # -c dev_tools/requirements/envs/dev.env.txt # tensorflow-docs +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via + # -c dev_tools/requirements/envs/dev.env.txt + # -r dev_tools/requirements/deps/pytest.txt pubchempy==1.0.5 \ --hash=sha256:08f0b2a82a5caa5d61e14935d655da554602d7b5686fe661ab584c882ffff623 \ --hash=sha256:e936cfed31fa194042ad463be3c803dde5b12ef2f795caf336e3114127c34fa0 diff --git a/dev_tools/requirements/envs/pytest.env.txt b/dev_tools/requirements/envs/pytest.env.txt index b116bc55b..bc704731f 100644 --- a/dev_tools/requirements/envs/pytest.env.txt +++ b/dev_tools/requirements/envs/pytest.env.txt @@ -1076,6 +1076,31 @@ protobuf==7.35.0 \ # via # -c dev_tools/requirements/envs/dev.env.txt # tensorflow-docs +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via + # -c dev_tools/requirements/envs/dev.env.txt + # -r dev_tools/requirements/deps/pytest.txt pubchempy==1.0.5 \ --hash=sha256:08f0b2a82a5caa5d61e14935d655da554602d7b5686fe661ab584c882ffff623 \ --hash=sha256:e936cfed31fa194042ad463be3c803dde5b12ef2f795caf336e3114127c34fa0 diff --git a/dev_tools/requirements/max_compat/pytest-max-compat.env.txt b/dev_tools/requirements/max_compat/pytest-max-compat.env.txt index 082214d2c..149899e82 100644 --- a/dev_tools/requirements/max_compat/pytest-max-compat.env.txt +++ b/dev_tools/requirements/max_compat/pytest-max-compat.env.txt @@ -992,6 +992,29 @@ protobuf==7.35.0 \ --hash=sha256:f05bcadf9a2a6b8dda047007075135fb7d08c73d9177aabc067e1be46881a201 \ --hash=sha256:fcbe42a4ac09d3ec9c987ddfcd956afd0b15f1ff613bd8371bde9405ffd5c8e5 # via tensorflow-docs +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via -r dev_tools/requirements/deps/pytest.txt pubchempy==1.0.5 \ --hash=sha256:08f0b2a82a5caa5d61e14935d655da554602d7b5686fe661ab584c882ffff623 \ --hash=sha256:e936cfed31fa194042ad463be3c803dde5b12ef2f795caf336e3114127c34fa0