Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/kickmix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,62 @@
- main
permissions: {}
jobs:
build_dist:
runs-on: ${{ matrix.os_dist.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
os_dist: [
{os: ubuntu-24.04, dist: cp310-manylinux_x86_64},
{os: ubuntu-24.04, dist: cp311-manylinux_x86_64},
{os: ubuntu-24.04, dist: cp312-manylinux_x86_64},
{os: ubuntu-24.04, dist: cp313-manylinux_x86_64},
{os: ubuntu-24.04, dist: cp314-manylinux_x86_64},
]
env:
CIBW_BUILD: "${{ matrix.os_dist.dist }}"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- run: python -m pip install pybind11==2.12.0 cibuildwheel~=3.3.1 setuptools wheel
- run: python -m cibuildwheel kickmix --print-build-identifiers
- run: python -m cibuildwheel kickmix --output-dir dist
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: "dist-kickmix-${{ matrix.os_dist.os }}-${{ matrix.os_dist.dist }}"
path: dist/*
build_sdist:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- run: python -m pip install setuptools pybind11==2.12.0 wheel
- run: cd kickmix && python setup.py sdist
- run: pip install kickmix/dist/*.tar.gz
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: "dist-kickmix-sdist"
path: kickmix/dist/*.tar.gz
merge_upload_artifacts:
needs: ["build_dist", "build_sdist"]
runs-on: ubuntu-24.04
permissions:
contents: read
actions: read
steps:
- name: Merge Artifacts
uses: actions/upload-artifact/merge@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist-kickmix
pattern: dist-kickmix-*
code_is_autoformatted:
runs-on: ubuntu-26.04
steps:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ parts/
sdist/
var/
wheels/
wheelhouse/
python_build_kickmix/
pip-wheel-metadata/
share/python-wheels/
wheelhouse/
python_build_kickmix/
*.egg-info/
.installed.cfg
*.egg
Expand Down
13 changes: 13 additions & 0 deletions docs/kickmix/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ pip install bazel-bin/kickmix/kickmix-0.0.dev0-py3-none-any.whl
PYTHONPATH=kickmix pytest kickmix/src
```

## Build Portable Python Wheel (with cibuildwheel)

```bash
sudo apt install podman
pip install cibuildwheel

# See https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip for --only values
STORAGE_OPTS="overlay.ignore_chown_errors=true" \
CIBW_CONTAINER_ENGINE=podman \
cibuildwheel kickmix --only cp313-manylinux_x86_64
# output is in wheelhouse/
```

## Auto-format code (using clang-format-21)

The project includes a `.clang-format` file that clang format will use automatically.
Expand Down
2 changes: 2 additions & 0 deletions kickmix/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
recursive-include src *.h *.inl *.cc
global-exclude *.test.cc *.test.h *.perf.cc *.perf.h *_test.py
24 changes: 24 additions & 0 deletions kickmix/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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.

[build-system]
requires = ["setuptools", "wheel", "pybind11==2.12.0"]
build-backend = "setuptools.build_meta"

[tool.cibuildwheel]
test-requires = ["pytest"]
test-command = "pytest {package}/src"

[tool.pytest.ini_options]
pythonpath = ["."]
72 changes: 72 additions & 0 deletions kickmix/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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.

import glob
import platform

import pybind11
from pybind11.setup_helpers import ParallelCompile
from setuptools import Extension, setup

ParallelCompile().install()

ALL_SOURCE_FILES = glob.glob("src/**/*.cc", recursive=True)
TEST_FILES = glob.glob("src/**/*.test.cc", recursive=True)
PERF_FILES = glob.glob("src/**/*.perf.cc", recursive=True)
MAIN_FILES = glob.glob("src/**/main.cc", recursive=True)
HEADER_FILES = glob.glob("src/**/*.h", recursive=True) + glob.glob("src/**/*.inl", recursive=True)
RELEVANT_SOURCE_FILES = sorted(set(ALL_SOURCE_FILES) - set(TEST_FILES + PERF_FILES + MAIN_FILES))

__version__ = '0.1.dev0'

if platform.system().startswith('Win'):
common_compile_args = [
'/std:c++20',
'/O2',
f'/DVERSION_INFO={__version__}',
]
else:
common_compile_args = [
'-std=c++20',
'-fno-strict-aliasing',
'-fvisibility=hidden',
'-O3',
'-g0',
f'-DVERSION_INFO={__version__}',
]
if any(arch in platform.machine().lower() for arch in ['x86', 'amd64', 'i386', 'i686']):
common_compile_args.append('-mno-avx')

kickmix_extension = Extension(
'kickmix',
sources=RELEVANT_SOURCE_FILES,
depends=HEADER_FILES,
include_dirs=[pybind11.get_include(), 'src'],
language='c++',
extra_compile_args=common_compile_args,
)

setup(
name='kickmix',
version=__version__,
author='Qualtran Developers',
url='https://github.com/quantumlib/Qualtran',
license='Apache-2.0',
description='Fast construction and simulation of quantum circuits with classical operations, phasing, and X basis demolition measurement.',
ext_modules=[kickmix_extension],
python_requires='>=3.10.0',
install_requires=['numpy'],
# Needed on Windows/macOS to avoid the default `build` colliding with Bazel's `BUILD`.
options={'build': {'build_base': 'python_build_kickmix'}},
)
56 changes: 22 additions & 34 deletions kickmix/src/kickmix/py/interactive_simulator.pybind.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,14 @@ static std::mt19937_64 make_rng(const pybind11::object &seed) {
throw pybind11::type_error(ss.str());
}
std::array<uint32_t, 8> seed_words{};
if (!seed.equal(pybind11::cast(0))) {
Py_ssize_t num_bytes = PyLong_AsNativeBytes(
seed.ptr(),
seed_words.data(),
sizeof(seed_words),
Py_ASNATIVEBYTES_LITTLE_ENDIAN | Py_ASNATIVEBYTES_REJECT_NEGATIVE | Py_ASNATIVEBYTES_UNSIGNED_BUFFER);
if (num_bytes < 0 || static_cast<size_t>(num_bytes) > sizeof(seed_words)) {
PyErr_Clear();
std::stringstream ss;
ss << "seed=" << pybind11::repr(seed) << " not in range(2**256)";
throw std::invalid_argument(ss.str());
}
try {
pybind11::bytes b = seed.attr("to_bytes")(sizeof(seed_words), "little");
memcpy(seed_words.data(), PyBytes_AsString(b.ptr()), sizeof(seed_words));
} catch (pybind11::error_already_set &) {
PyErr_Clear();
std::stringstream ss;
ss << "seed=" << pybind11::repr(seed) << " not in range(2**256)";
throw std::invalid_argument(ss.str());
}
std::seed_seq seq(seed_words.begin(), seed_words.end());
return std::mt19937_64(seq);
Expand Down Expand Up @@ -88,24 +84,19 @@ void int_obj_to_bytes_with_max_bits(
}

size_t max_bytes = (max_bits + 7) / 8;
if (int_obj.equal(pybind11::cast(0))) {
// Need this special case because PyLong_AsNativeBytes claims 0 takes 8 bytes to store rather than 0.
memset(bytes_buffer, 0, max_bytes);
return;
}

Py_ssize_t non_zero_bytes = PyLong_AsNativeBytes(
int_obj.ptr(),
bytes_buffer,
max_bytes,
Py_ASNATIVEBYTES_LITTLE_ENDIAN | Py_ASNATIVEBYTES_REJECT_NEGATIVE | Py_ASNATIVEBYTES_UNSIGNED_BUFFER);
bool has_extra = false;
if (max_bits % 8) {
uint8_t mask = 0xFF >> (8 - (max_bits % 8));
uint8_t b = bytes_buffer[max_bytes - 1];
has_extra = (b & mask) != b;
bool has_err = false;
try {
pybind11::bytes b = int_obj.attr("to_bytes")(max_bytes, "little");
memcpy(bytes_buffer, PyBytes_AsString(b.ptr()), max_bytes);
if (max_bits % 8) {
uint8_t mask = 0xFF >> (8 - (max_bits % 8));
uint8_t last_byte = bytes_buffer[max_bytes - 1];
has_err = (last_byte & mask) != last_byte;
}
} catch (pybind11::error_already_set &) {
PyErr_Clear();
has_err = true;
}
bool has_err = non_zero_bytes < 0 || (size_t)non_zero_bytes > max_bytes || has_extra;
if (has_err) {
std::stringstream ss;
ss << value_name << "=" << pybind11::repr(int_obj);
Expand All @@ -125,11 +116,8 @@ pybind11::object int_obj_from_bytes_with_max_bits(uint8_t *bytes_buffer, size_t
uint8_t mask = 0xFF >> (8 - (max_bits % 8));
bytes_buffer[bytes_len - 1] &= mask;
}
PyObject *py_long = PyLong_FromUnsignedNativeBytes(bytes_buffer, bytes_len, Py_ASNATIVEBYTES_LITTLE_ENDIAN);
if (!py_long) {
throw pybind11::error_already_set();
}
return pybind11::reinterpret_steal<pybind11::object>(py_long);
pybind11::bytes b(reinterpret_cast<const char *>(bytes_buffer), bytes_len);
return pybind11::reinterpret_borrow<pybind11::object>((PyObject *)&PyLong_Type).attr("from_bytes")(b, "little");
}

PyInteractiveSimulator::PyInteractiveSimulator(
Expand Down
Loading