Skip to content

Commit

Permalink
Allow sparse pauli syntax in stim.PauliString.__init__ (#695)
Browse files Browse the repository at this point in the history
- For example, `stim.PauliString("X2*Y5") == stim.PauliString("__X__Y")`
- Also allow sparse pauli syntax in `stim.has_flow`
- Add `stim::simd_bits{_range_ref}.as_u64` convenience method
- Refactor `stim::PauliString::safe_accumulate_pauli_term` into
`{right,left}_mul_pauli`
  • Loading branch information
Strilanc authored Feb 15, 2024
1 parent 6cda399 commit c2637f7
Show file tree
Hide file tree
Showing 17 changed files with 529 additions and 120 deletions.
44 changes: 31 additions & 13 deletions doc/python_api_reference_vDev.md
Original file line number Diff line number Diff line change
Expand Up @@ -1907,20 +1907,19 @@ def has_flow(
a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and
the CNOT flows implemented by the circuit involve these measurements.
A flow like P -> Q means that the circuit transforms P into Q.
A flow like 1 -> P means that the circuit prepares P.
A flow like P -> 1 means that the circuit measures P.
A flow like 1 -> 1 means that the circuit contains a detector.
A flow like P -> Q means the circuit transforms P into Q.
A flow like 1 -> P means the circuit prepares P.
A flow like P -> 1 means the circuit measures P.
A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR).
Args:
shorthand: Specifies the flow as a short string like "IX -> -YZ xor rec[1]".
shorthand: Specifies the flow as a short string like "X1 -> -YZ xor rec[1]".
The text must contain "->" to separate the input pauli string from the
output pauli string. Each pauli string should be a sequence of
characters from "_IXYZ" (or else just "1" to indicate the empty Pauli
string) optionally prefixed by "+" or "-". Measurements are included
by appending " xor rec[k]" for each measurement index k. Indexing uses
the python convention where non-negative indices index from the start
and negative indices index from the end.
output pauli string. Measurements are included by appending
" xor rec[k]" for each measurement index k. Indexing uses the python
convention where non-negative indices index from the start and negative
indices index from the end. The pauli strings are parsed as if by
`stim.PauliString.__init__`.
start: The input into the flow at the start of the circuit. Defaults to None
(the identity Pauli string). When specified, this should be a
`stim.PauliString`, or a `str` (which will be parsed using
Expand Down Expand Up @@ -1961,6 +1960,16 @@ def has_flow(
False
>>> m.has_flow('Z -> I xor rec[-1]')
True
>>> m.has_flow('Z -> rec[-1]')
True
>>> cx58 = stim.Circuit('CX 5 8')
>>> cx58.has_flow('X5 -> X5*X8')
True
>>> cx58.has_flow('X_ -> XX')
False
>>> cx58.has_flow('_____X___ -> _____X__X')
True
>>> stim.Circuit('''
... RY 0
Expand Down Expand Up @@ -8094,8 +8103,11 @@ def __init__(
When given a string, the string is parsed as a pauli string. The string can
optionally start with a sign ('+', '-', 'i', '+i', or '-i'). The rest of the
string should be characters from '_IXYZ' where '_' and 'I' mean identity, 'X'
means Pauli X, 'Y' means Pauli Y, and 'Z' means Pauli Z.
string should be either a dense pauli string or a sparse pauli string. A dense
pauli string is made up of characters from '_IXYZ' where '_' and 'I' mean
identity, 'X' means Pauli X, 'Y' means Pauli Y, and 'Z' means Pauli Z. A sparse
pauli string is a series of integers seperated by '*' and prefixed by 'I', 'X',
'Y', or 'Z'.
Arguments:
arg [position-only]: This can be a variety of types, including:
Expand Down Expand Up @@ -8127,6 +8139,12 @@ def __init__(
>>> stim.PauliString("X" for _ in range(4))
stim.PauliString("+XXXX")
>>> stim.PauliString("-X2*Y6")
stim.PauliString("-__X___Y")
>>> stim.PauliString("X6*Y6")
stim.PauliString("+i______Z")
"""
```

Expand Down
44 changes: 31 additions & 13 deletions doc/stim.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1328,20 +1328,19 @@ class Circuit:
a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and
the CNOT flows implemented by the circuit involve these measurements.
A flow like P -> Q means that the circuit transforms P into Q.
A flow like 1 -> P means that the circuit prepares P.
A flow like P -> 1 means that the circuit measures P.
A flow like 1 -> 1 means that the circuit contains a detector.
A flow like P -> Q means the circuit transforms P into Q.
A flow like 1 -> P means the circuit prepares P.
A flow like P -> 1 means the circuit measures P.
A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR).
Args:
shorthand: Specifies the flow as a short string like "IX -> -YZ xor rec[1]".
shorthand: Specifies the flow as a short string like "X1 -> -YZ xor rec[1]".
The text must contain "->" to separate the input pauli string from the
output pauli string. Each pauli string should be a sequence of
characters from "_IXYZ" (or else just "1" to indicate the empty Pauli
string) optionally prefixed by "+" or "-". Measurements are included
by appending " xor rec[k]" for each measurement index k. Indexing uses
the python convention where non-negative indices index from the start
and negative indices index from the end.
output pauli string. Measurements are included by appending
" xor rec[k]" for each measurement index k. Indexing uses the python
convention where non-negative indices index from the start and negative
indices index from the end. The pauli strings are parsed as if by
`stim.PauliString.__init__`.
start: The input into the flow at the start of the circuit. Defaults to None
(the identity Pauli string). When specified, this should be a
`stim.PauliString`, or a `str` (which will be parsed using
Expand Down Expand Up @@ -1382,6 +1381,16 @@ class Circuit:
False
>>> m.has_flow('Z -> I xor rec[-1]')
True
>>> m.has_flow('Z -> rec[-1]')
True
>>> cx58 = stim.Circuit('CX 5 8')
>>> cx58.has_flow('X5 -> X5*X8')
True
>>> cx58.has_flow('X_ -> XX')
False
>>> cx58.has_flow('_____X___ -> _____X__X')
True
>>> stim.Circuit('''
... RY 0
Expand Down Expand Up @@ -6212,8 +6221,11 @@ class PauliString:
When given a string, the string is parsed as a pauli string. The string can
optionally start with a sign ('+', '-', 'i', '+i', or '-i'). The rest of the
string should be characters from '_IXYZ' where '_' and 'I' mean identity, 'X'
means Pauli X, 'Y' means Pauli Y, and 'Z' means Pauli Z.
string should be either a dense pauli string or a sparse pauli string. A dense
pauli string is made up of characters from '_IXYZ' where '_' and 'I' mean
identity, 'X' means Pauli X, 'Y' means Pauli Y, and 'Z' means Pauli Z. A sparse
pauli string is a series of integers seperated by '*' and prefixed by 'I', 'X',
'Y', or 'Z'.
Arguments:
arg [position-only]: This can be a variety of types, including:
Expand Down Expand Up @@ -6245,6 +6257,12 @@ class PauliString:
>>> stim.PauliString("X" for _ in range(4))
stim.PauliString("+XXXX")
>>> stim.PauliString("-X2*Y6")
stim.PauliString("-__X___Y")
>>> stim.PauliString("X6*Y6")
stim.PauliString("+i______Z")
"""
def __itruediv__(
self,
Expand Down
44 changes: 31 additions & 13 deletions glue/python/src/stim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1328,20 +1328,19 @@ class Circuit:
a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and
the CNOT flows implemented by the circuit involve these measurements.
A flow like P -> Q means that the circuit transforms P into Q.
A flow like 1 -> P means that the circuit prepares P.
A flow like P -> 1 means that the circuit measures P.
A flow like 1 -> 1 means that the circuit contains a detector.
A flow like P -> Q means the circuit transforms P into Q.
A flow like 1 -> P means the circuit prepares P.
A flow like P -> 1 means the circuit measures P.
A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR).
Args:
shorthand: Specifies the flow as a short string like "IX -> -YZ xor rec[1]".
shorthand: Specifies the flow as a short string like "X1 -> -YZ xor rec[1]".
The text must contain "->" to separate the input pauli string from the
output pauli string. Each pauli string should be a sequence of
characters from "_IXYZ" (or else just "1" to indicate the empty Pauli
string) optionally prefixed by "+" or "-". Measurements are included
by appending " xor rec[k]" for each measurement index k. Indexing uses
the python convention where non-negative indices index from the start
and negative indices index from the end.
output pauli string. Measurements are included by appending
" xor rec[k]" for each measurement index k. Indexing uses the python
convention where non-negative indices index from the start and negative
indices index from the end. The pauli strings are parsed as if by
`stim.PauliString.__init__`.
start: The input into the flow at the start of the circuit. Defaults to None
(the identity Pauli string). When specified, this should be a
`stim.PauliString`, or a `str` (which will be parsed using
Expand Down Expand Up @@ -1382,6 +1381,16 @@ class Circuit:
False
>>> m.has_flow('Z -> I xor rec[-1]')
True
>>> m.has_flow('Z -> rec[-1]')
True
>>> cx58 = stim.Circuit('CX 5 8')
>>> cx58.has_flow('X5 -> X5*X8')
True
>>> cx58.has_flow('X_ -> XX')
False
>>> cx58.has_flow('_____X___ -> _____X__X')
True
>>> stim.Circuit('''
... RY 0
Expand Down Expand Up @@ -6212,8 +6221,11 @@ class PauliString:
When given a string, the string is parsed as a pauli string. The string can
optionally start with a sign ('+', '-', 'i', '+i', or '-i'). The rest of the
string should be characters from '_IXYZ' where '_' and 'I' mean identity, 'X'
means Pauli X, 'Y' means Pauli Y, and 'Z' means Pauli Z.
string should be either a dense pauli string or a sparse pauli string. A dense
pauli string is made up of characters from '_IXYZ' where '_' and 'I' mean
identity, 'X' means Pauli X, 'Y' means Pauli Y, and 'Z' means Pauli Z. A sparse
pauli string is a series of integers seperated by '*' and prefixed by 'I', 'X',
'Y', or 'Z'.
Arguments:
arg [position-only]: This can be a variety of types, including:
Expand Down Expand Up @@ -6245,6 +6257,12 @@ class PauliString:
>>> stim.PauliString("X" for _ in range(4))
stim.PauliString("+XXXX")
>>> stim.PauliString("-X2*Y6")
stim.PauliString("-__X___Y")
>>> stim.PauliString("X6*Y6")
stim.PauliString("+i______Z")
"""
def __itruediv__(
self,
Expand Down
31 changes: 20 additions & 11 deletions src/stim/circuit/circuit.pybind.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2335,20 +2335,19 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_<Ci
a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and
the CNOT flows implemented by the circuit involve these measurements.
A flow like P -> Q means that the circuit transforms P into Q.
A flow like 1 -> P means that the circuit prepares P.
A flow like P -> 1 means that the circuit measures P.
A flow like 1 -> 1 means that the circuit contains a detector.
A flow like P -> Q means the circuit transforms P into Q.
A flow like 1 -> P means the circuit prepares P.
A flow like P -> 1 means the circuit measures P.
A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR).
Args:
shorthand: Specifies the flow as a short string like "IX -> -YZ xor rec[1]".
shorthand: Specifies the flow as a short string like "X1 -> -YZ xor rec[1]".
The text must contain "->" to separate the input pauli string from the
output pauli string. Each pauli string should be a sequence of
characters from "_IXYZ" (or else just "1" to indicate the empty Pauli
string) optionally prefixed by "+" or "-". Measurements are included
by appending " xor rec[k]" for each measurement index k. Indexing uses
the python convention where non-negative indices index from the start
and negative indices index from the end.
output pauli string. Measurements are included by appending
" xor rec[k]" for each measurement index k. Indexing uses the python
convention where non-negative indices index from the start and negative
indices index from the end. The pauli strings are parsed as if by
`stim.PauliString.__init__`.
start: The input into the flow at the start of the circuit. Defaults to None
(the identity Pauli string). When specified, this should be a
`stim.PauliString`, or a `str` (which will be parsed using
Expand Down Expand Up @@ -2389,6 +2388,16 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_<Ci
False
>>> m.has_flow('Z -> I xor rec[-1]')
True
>>> m.has_flow('Z -> rec[-1]')
True
>>> cx58 = stim.Circuit('CX 5 8')
>>> cx58.has_flow('X5 -> X5*X8')
True
>>> cx58.has_flow('X_ -> XX')
False
>>> cx58.has_flow('_____X___ -> _____X__X')
True
>>> stim.Circuit('''
... RY 0
Expand Down
25 changes: 9 additions & 16 deletions src/stim/circuit/stabilizer_flow.inl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "stim/simulators/frame_simulator_util.h"
#include "stim/simulators/sparse_rev_frame_tracker.h"
#include "stim/simulators/tableau_simulator.h"
#include "stim/stabilizers/flex_pauli_string.h"

namespace stim {

Expand Down Expand Up @@ -104,22 +105,14 @@ PauliString<W> parse_non_empty_pauli_string_allowing_i(std::string_view text, bo
throw std::invalid_argument("Got an ambiguously blank pauli string. Use '1' for the empty Pauli string.");
}

bool negate = false;
if (text.starts_with('i')) {
*imag_out = true;
text = text.substr(1);
} else if (text.starts_with("-i")) {
negate = true;
*imag_out = true;
text = text.substr(2);
} else if (text.starts_with("+i")) {
*imag_out = true;
text = text.substr(2);
}
PauliString<W> result = PauliString<W>::from_str(text);
if (negate) {
result.sign ^= 1;
}
auto flex = FlexPauliString::from_text(text);
*imag_out = flex.imag;

PauliString<W> result(flex.value.num_qubits);
size_t nb = std::min(flex.value.xs.num_u8_padded(), result.xs.num_u8_padded());
memcpy(result.xs.u8, flex.value.xs.u8, nb);
memcpy(result.zs.u8, flex.value.zs.u8, nb);
result.sign = flex.value.sign;
return result;
}

Expand Down
2 changes: 2 additions & 0 deletions src/stim/mem/simd_bits.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ struct simd_bits {
inline size_t num_bits_padded() const {
return num_simd_words * W;
}

uint64_t as_u64() const;
};

template <size_t W>
Expand Down
5 changes: 5 additions & 0 deletions src/stim/mem/simd_bits.inl
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ size_t simd_bits<W>::popcnt() const {
return simd_bits_range_ref<W>(*this).popcnt();
}

template <size_t W>
uint64_t simd_bits<W>::as_u64() const {
return simd_bits_range_ref<W>(*this).as_u64();
}

template <size_t W>
size_t simd_bits<W>::countr_zero() const {
return simd_bits_range_ref<W>(*this).countr_zero();
Expand Down
3 changes: 3 additions & 0 deletions src/stim/mem/simd_bits_range_ref.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ struct simd_bits_range_ref {
bool operator!=(const simd_bits_range_ref<W> &other) const;
/// Determines whether or not any of the bits in the referenced range are non-zero.
bool not_zero() const;
/// Treats the referenced range as an unsigned integer, and returns it as a uint64_t.
/// If the integer is too large to fit, an exception is raised.
uint64_t as_u64() const;

/// Returns a reference to a given bit within the referenced range.
inline bit_ref operator[](size_t k) {
Expand Down
16 changes: 16 additions & 0 deletions src/stim/mem/simd_bits_range_ref.inl
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,20 @@ bool simd_bits_range_ref<W>::intersects(const simd_bits_range_ref<W> other) cons
return v != 0;
}

template <size_t W>
uint64_t simd_bits_range_ref<W>::as_u64() const {
size_t n64 = num_u64_padded();
for (size_t k = 1; k < n64; k++) {
if (u64[k]) {
throw std::invalid_argument("Too large to fit into a uint64_t.");
}
}
size_t n1 = std::min(num_bits_padded(), (size_t)64);
uint64_t v = 0;
for (size_t k = 0; k < n1; k++) {
v ^= uint64_t{(*this)[k]} << k;
}
return v;
}

} // namespace stim
24 changes: 24 additions & 0 deletions src/stim/mem/simd_bits_range_ref.test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,27 @@ TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, prefix_ref, {
prefix[0] = true;
ASSERT_TRUE(data[0]);
})

TEST_EACH_WORD_SIZE_W(simd_bits_range_ref, as_u64, {
simd_bits<W> data(1024);
simd_bits_range_ref<W> ref(data);
ASSERT_EQ(data.as_u64(), 0);
ASSERT_EQ(ref.as_u64(), 0);

ref[63] = 1;
ASSERT_EQ(data.as_u64(), uint64_t{1} << 63);
ASSERT_EQ(ref.as_u64(), uint64_t{1} << 63);
ASSERT_EQ(data.word_range_ref(0, 0).as_u64(), 0);
ASSERT_EQ(data.word_range_ref(0, 1).as_u64(), uint64_t{1} << 63);
ASSERT_EQ(data.word_range_ref(0, 2).as_u64(), uint64_t{1} << 63);
ASSERT_EQ(data.word_range_ref(1, 1).as_u64(), 0);
ASSERT_EQ(data.word_range_ref(1, 2).as_u64(), 0);

ref[64] = 1;
ASSERT_THROW({ data.as_u64(); }, std::invalid_argument);
ASSERT_THROW({ data.word_range_ref(0, 2).as_u64(); }, std::invalid_argument);
ASSERT_THROW({ ref.as_u64(); }, std::invalid_argument);
if (data.num_simd_words > 2) {
ASSERT_EQ(data.word_range_ref(2, 1).as_u64(), 0);
}
})
Loading

0 comments on commit c2637f7

Please sign in to comment.