From 4c5e22d2f3d707e0aafaaad01de4f75e70897f29 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Wed, 14 Oct 2026 19:36:12 -0700 Subject: [PATCH 1/2] Polish the kickmix API boundary - Fix the remaining missing examples in docstrings - Delete {start,end}_auto_free_scope - Fix some missing ensure_big_enough_state_for calls that were causing simulator writes to fail --- dev_tools/kickmix/util_gen_stub_file.py | 1 + docs/kickmix/api_reference.md | 525 +++++++++++++++--- kickmix/src/kickmix/id/qubit_or_bit_or_bool.h | 3 + .../src/kickmix/py/circuit/circuit.pybind.cc | 167 +++++- .../py/circuit/circuit_builder.pybind.cc | 271 ++++++--- .../src/kickmix/py/circuit/circuit_test.py | 24 + .../py/interactive_simulator.pybind.cc | 53 ++ kickmix/src/kickmix/py/main.pybind.cc | 70 ++- kickmix/src/kickmix/py/val/array.pybind.cc | 18 + kickmix/src/kickmix/py/val/id.pybind.cc | 20 + 10 files changed, 975 insertions(+), 177 deletions(-) diff --git a/dev_tools/kickmix/util_gen_stub_file.py b/dev_tools/kickmix/util_gen_stub_file.py index fba71756d..3820625a1 100644 --- a/dev_tools/kickmix/util_gen_stub_file.py +++ b/dev_tools/kickmix/util_gen_stub_file.py @@ -21,6 +21,7 @@ keep = { "__add__", + "__bool__", "__ipow__", "__radd__", "__eq__", diff --git a/docs/kickmix/api_reference.md b/docs/kickmix/api_reference.md index 4802de71e..9024617c2 100644 --- a/docs/kickmix/api_reference.md +++ b/docs/kickmix/api_reference.md @@ -41,7 +41,6 @@ - [`kickmix.CircuitBuilder.del_gf2_inverse_with_scaffold`](#kickmix.CircuitBuilder.del_gf2_inverse_with_scaffold) - [`kickmix.CircuitBuilder.del_gf2_mul`](#kickmix.CircuitBuilder.del_gf2_mul) - [`kickmix.CircuitBuilder.del_lookup`](#kickmix.CircuitBuilder.del_lookup) - - [`kickmix.CircuitBuilder.end_auto_free_scope`](#kickmix.CircuitBuilder.end_auto_free_scope) - [`kickmix.CircuitBuilder.finish_circuit`](#kickmix.CircuitBuilder.finish_circuit) - [`kickmix.CircuitBuilder.flip_if_equal`](#kickmix.CircuitBuilder.flip_if_equal) - [`kickmix.CircuitBuilder.flip_if_greater_than`](#kickmix.CircuitBuilder.flip_if_greater_than) @@ -77,7 +76,6 @@ - [`kickmix.CircuitBuilder.reset`](#kickmix.CircuitBuilder.reset) - [`kickmix.CircuitBuilder.right_rotate`](#kickmix.CircuitBuilder.right_rotate) - [`kickmix.CircuitBuilder.set_max_qubits`](#kickmix.CircuitBuilder.set_max_qubits) - - [`kickmix.CircuitBuilder.start_auto_free_scope`](#kickmix.CircuitBuilder.start_auto_free_scope) - [`kickmix.CircuitBuilder.swap`](#kickmix.CircuitBuilder.swap) - [`kickmix.CircuitBuilder.unary_iteration`](#kickmix.CircuitBuilder.unary_iteration) - [`kickmix.CircuitBuilder.x`](#kickmix.CircuitBuilder.x) @@ -170,6 +168,7 @@ - [`kickmix.xb.__str__`](#kickmix.xb.__str__) - [`kickmix.xb.id`](#kickmix.xb.id) - [`kickmix.xbool`](#kickmix.xbool) + - [`kickmix.xbool.__bool__`](#kickmix.xbool.__bool__) - [`kickmix.xbool.__eq__`](#kickmix.xbool.__eq__) - [`kickmix.xbool.__hash__`](#kickmix.xbool.__hash__) - [`kickmix.xbool.__init__`](#kickmix.xbool.__init__) @@ -191,7 +190,21 @@ import numpy as np # (at top-level in the kickmix module) class Circuit: - """A kickmix circuit. + """An immutable kickmix circuit. + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... X q0 + ... CX q0 q9 + ... ''') + + >>> simulator = km.Simulator(batch_size=1) + >>> print(simulator.read_within_shot(km.q(9), shot_index=0)) + False + >>> simulator.do(circuit) + >>> print(simulator.read_within_shot(km.q(9), shot_index=0)) + True """ ``` @@ -286,7 +299,27 @@ def __init__( self, circuit_text: str = '', ) -> None: - """Creates a kickmix Circuit by parsing the given text. + """Creates a circuit by parsing the given text as kickmix circuit file. + + For a definition of the expected text format, see the kickmix circuit file + format documentation at + https://github.com/quantumlib/Qualtran/blob/main/docs/kickmix/kickmix_format.md + + Roughly speaking, it's just a series of lines like "CCX q0 q1 q2". + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... X q0 + ... CX q0 q9 + ... ''') + + >>> simulator = km.Simulator(batch_size=1) + >>> print(simulator.read_within_shot(km.q(9), shot_index=0)) + False + >>> simulator.do(circuit) + >>> print(simulator.read_within_shot(km.q(9), shot_index=0)) + True """ ``` @@ -299,6 +332,32 @@ def __len__( self, ) -> int: """Returns the number of instructions in the circuit. + + Caution: register data is not counted as instructions. + + Examples: + >>> import kickmix as km + >>> len(km.Circuit()) + 0 + + >>> len(km.Circuit(''' + ... X q0 + ... CX q0 q9 + ... ''')) + 2 + + >>> len(km.Circuit(''' + ... REGISTER r0 "test" + ... APPEND_TO_REGISTER q0 r0 + ... APPEND_TO_REGISTER q1 r0 + ... APPEND_TO_REGISTER q2 r0 + ... INFO_TAG "@enter:increment" + ... CCX q0 q1 q2 + ... CX q0 q1 + ... X q0 + ... INFO_TAG "@exit:increment" + ... ''')) + 5 """ ``` @@ -325,8 +384,7 @@ def __mul__( ''') >>> km.Circuit('CX q0 q1') * 0 - km.Circuit(''' - ''') + km.Circuit() >>> 5 * km.Circuit(''' ... REGISTER r0 "test" @@ -403,8 +461,7 @@ def __rmul__( ''') >>> km.Circuit('CX q0 q1') * 0 - km.Circuit(''' - ''') + km.Circuit() >>> 5 * km.Circuit(''' ... REGISTER r0 "test" @@ -544,6 +601,16 @@ def html_diagram( self, ) -> str: """Returns an html diagram of the circuit. + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... X q0 + ... CX q0 q1 + ... ''') + >>> html = circuit.html_diagram() + >>> ' int: - """Returns an upper bound on the number of CCX/CCZ gates run by the circuit. + """Returns an upper bound on the number of CCX/CCZ gates run by the + circuit. + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... X q0 + ... CX q0 q1 + ... CCX q0 q1 q2 + ... CCZ q0 q1 q2 + ... ''') + >>> circuit.max_magic() + 2 """ ``` @@ -606,7 +685,17 @@ def num_registers( def reaction_depth( self, ) -> int: - """Returns the reaction depth of the circuit, assuming it is powered by CCZ states. + """Returns the reaction depth of the circuit, assuming it is powered by + CCZ states. + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... CCX q0 q1 q2 + ... CCX q2 q3 q4 + ... ''') + >>> circuit.reaction_depth() + 2 """ ``` @@ -619,7 +708,32 @@ def reaction_depth( def register_data( self, ) -> object: - """Returns the names and contents of registers in the circuit. + """The names and contents of registers in the circuit, as a dict. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + + >>> circuit = km.Circuit(''' + ... REGISTER r0 "test" + ... APPEND_TO_REGISTER q0 r0 + ... APPEND_TO_REGISTER q1 r0 + ... APPEND_TO_REGISTER q2 r0 + ... REGISTER r2 "empty" + ... REGISTER r3 + ... APPEND_TO_REGISTER q3 r3 + ... APPEND_TO_REGISTER b0 r3 + ... CCX q0 q1 q2 + ... CX q0 q1 + ... X q0 + ... ''') + >>> data: dict[str, km.array] = circuit.register_data + >>> for k, v in data.items(): + ... print(f"{k!r}: {v!r}") + 'test': km.array([km.q(0), km.q(1), km.q(2)]) + km.r(1): km.array([]) + 'empty': km.array([]) + km.r(3): km.array([km.q(3), km.b(0)]) """ ``` @@ -632,6 +746,20 @@ def text_diagram( self, ) -> str: """Returns a text diagram of the circuit. + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... X q0 + ... CX q0 q1 + ... CCX q0 q1 q2 + ... ''') + >>> print(circuit.text_diagram()) + q0: -X-@-@- + | | + q1: ---X-@- + | + q2: -----X- """ ``` @@ -681,7 +809,35 @@ def to_file( # (at top-level in the kickmix module) class CircuitBuilder: - """A kickmix circuit builder. + """A builder class that helps create a kickmix circuit. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + + >>> target = builder.create_quantum_register(7, "target") + >>> shift = builder.create_quantum_register(3, "shift") + >>> builder.left_rotate(target, shift) + >>> print(builder.finish_circuit().text_diagram()) + q0: -target[0]-@-X-@---------@-X-@-----------@---X---@-----@-X-@----------- + | | | | | | | | | | | | + q1: -target[1]-X-@-X---------|-|-|-----------|-@-|-X-|-@---X-@-X----------- + | | | | | | | | | | | + q2: -target[2]---|-@---X---@-X-@-X-----------|-|-|-|-|-|-----|-@---X---@--- + | | | | | | | | | | | | | | | + q3: -target[3]---|-|-@-|-X-|-@-|-@---X---@---|-X-|-@-|-X-----|-|-@-|-X-|-@- + | | | | | | | | | | | | | | | | | | | | | | + q4: -target[4]---|-|-|-|-|-|-|-|-|-@-|-X-|-@-X---@-|-X-------|-|-|-|-|-|-|- + | | | | | | | | | | | | | | | | | | | | | | | + q5: -target[5]---|-|-X-|-@-|-X-|-|-X-|-@-|-X-----|-|---@-X-@-|-|-X-|-@-|-X- + | | | | | | | | | | | | | | | | | | | | + q6: -target[6]---|-X---@-|-X---|-X---@-|-X-------|-|---X-@-X-|-X---@-|-X--- + | | | | | | | | | | | | + q7: -shift[0]----@-----@-@---@-|-----|-|---@-----|-|-----|---|-----|-|----- + | | | | | | | | | | | + q8: -shift[1]----------------X-@-----@-@---X-@---|-|-----|-@-|-----|-|----- + | | | | | | | | + q9: -shift[2]--------------------------------X---@-@-----@-X-@-----@-@----- """ ``` @@ -694,6 +850,15 @@ def __init__( self, ) -> None: """Initializes a new circuit builder. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> builder.x(km.q(0)) + >>> builder.finish_circuit() + km.Circuit(''' + X q0 + ''') """ ``` @@ -719,23 +884,33 @@ def alloc_qubits( ) -> kickmix.array: """Allocates clean qubits to use as workspace in a circuit. - Allocated qubits can be deallocated by using the `CircuitBuilder.free` method. - Qubits allocated within a `with builder.scope(auto_free=True):` block are - deallocated automatically upon execution exiting the block. + Allocated qubits can be deallocated by using the `CircuitBuilder.free` + method. Qubits allocated within a `with builder.scope(auto_free=True):` + block are deallocated automatically upon execution exiting the block. - Although the qubits returned by this method *should* be clean, nothing *enforces* - this. If a prior user of the allocated qubit failed to correctly clean some qubits - before freeing them, the returned qubits may contain their buggy garbage. As such, - it's recommended that you reset the qubits before using them. Reset gates also - have the nice side effect of clearly indicating in circuit diagrams when a new - usage of a qubit has begun. + Although the qubits returned by this method *should* be clean, nothing + *enforces* this. If a prior user of the allocated qubit failed to + correctly clean some qubits before freeing them, the returned qubits may + contain their buggy garbage. As such, it's recommended that you reset + the qubits before using them. Reset gates also have the nice side effect + of clearly indicating in circuit diagrams when a new usage of a qubit + has begun. Args: - length: The number of qubits to allocate. + count: The number of qubits to allocate. Returns: - An array containing the ids of the allocated qubits. Until these ids are - freed, they won't be returned by any other allocation calls. + An array containing the ids of the allocated qubits. Until these ids + are freed, they won't be returned by any other allocation calls. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> q = builder.alloc_qubits(3) + >>> q + km.array([km.q(0), km.q(1), km.q(2)]) + >>> builder.num_allocated_qubits + 3 """ ``` @@ -864,15 +1039,30 @@ def create_classical_register( ) -> kickmix.array: """Creates a classical register. - Reserves the given number of bits for the register, and emits instructions - defining the register. + Reserves the given number of bits for the register, and emits + instructions defining the register. Args: - name: A name for the register, which can be used to refer to it. length: The number of bits in the register. + name: A name for the register, which can be used to refer to it. Returns: - A kickmix.CircuitRegister containing the allocated qubits. + A km.array containing the allocated bits. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> reg = builder.create_classical_register(2, name='meas') + >>> reg + km.array([km.b(0), km.b(1)]) + >>> builder.bit_store(reg[0], True) + >>> builder.finish_circuit() + km.Circuit(''' + APPEND_TO_REGISTER b0 r0 + APPEND_TO_REGISTER b1 r0 + REGISTER r0 "meas" + BIT_STORE1 b0 + ''') """ ``` @@ -888,15 +1078,30 @@ def create_quantum_register( ) -> kickmix.array: """Creates a quantum register. - Reserves the given number of qubits for the register, and emits instructions - defining the register. + Reserves the given number of qubits for the register, and emits + instructions defining the register. Args: - name: A name for the register, which can be used to refer to it. length: The number of qubits in the register. + name: A name for the register, which can be used to refer to it. Returns: - A kickmix.CircuitRegister containing the allocated qubits. + A km.array containing the allocated qubits. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> reg = builder.create_quantum_register(2, name='data') + >>> reg + km.array([km.q(0), km.q(1)]) + >>> builder.x(reg[0]) + >>> builder.finish_circuit() + km.Circuit(''' + APPEND_TO_REGISTER q0 r0 + APPEND_TO_REGISTER q1 r0 + REGISTER r0 "data" + X q0 + ''') """ ``` @@ -970,19 +1175,29 @@ def cz( ) -> None: """Appends many CZ gates to the circuit. - This method broadcasts over km.array arguments. If any of the arguments is a - km.array, an append is performed for each array index. When multiple km.array - arguments are used, they are combined by zipping (i.e. using two arrays of - length n will produce n instructions rather than n*n instructions). + This method broadcasts over km.array arguments. If any of the arguments + is a km.array, an append is performed for each array index. When + multiple km.array arguments are used, they are combined by zipping + (i.e. using two arrays of length n will produce n instructions rather + than n*n instructions). - The exact kind of operation(s) appended to the circuit depends on the types of - the controls. For example, if one of the controls is the boolean value True - instead of a qubit, then a Z instruction will be appended instead of a CZ - instruction. + The exact kind of operation(s) appended to the circuit depends on the + types of the controls. For example, if one of the controls is the + boolean value True instead of a qubit, then a Z instruction will be + appended instead of a CZ instruction. Args: control1: One of the controls determining if phasing occurs. control2: The other controls determining if phasing occurs. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> builder.cz(km.q(0), km.q(1)) + >>> builder.finish_circuit() + km.Circuit(''' + CZ q0 q1 + ''') """ ``` @@ -1257,34 +1472,37 @@ def del_lookup( ) -> None: """Appends operations to perform `del target := table[address]`. - Clears a value created by looking up a value from a classical table, indexed by - a quantum register. + Clears a value created by looking up a value from a classical table, + indexed by a quantum register. - Because this is a `del` method, the target qubits will end in the 0 state. + Because this is a `del` method, the target qubits will end in the 0 + state. Args: table: The table of classical values, as a list of python integers. - The integers are interpreted in a little-endian fashion, meaning that - `table_data[address, out_index] == (table[address] >> out_index) & 1`. + The integers are interpreted in a little-endian fashion, meaning + that `table_data[address, out_index] == + (table[address] >> out_index) & 1`. address: The address used to select a value from the table. Qubits are in little-endian order. target: The qubits to clear the lookup result from. - """ -``` - -```python -# kickmix.CircuitBuilder.end_auto_free_scope - -# (in class kickmix.CircuitBuilder) -def end_auto_free_scope( - self, -) -> None: - """Ends a scope that automatically frees allocations. - - Allocations since the corresponding builder.start_auto_free_scope() will be - freed. If multiple scopes are being used, they operate in LIFO order (i.e. like - a stack). + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> address = builder.create_quantum_register(2, name='address') + >>> target = builder.create_quantum_register(4, name='target') + >>> table = [2, 3, 5, 7] + >>> builder.init_lookup(table=table, address=address, target=target) + >>> builder.del_lookup(table=table, address=address, target=target) + >>> circuit = builder.finish_circuit() + >>> sim = km.Simulator(batch_size=4) + >>> sim.use_same_registers_as(circuit) + >>> for a in range(4): + ... sim.write_within_shot('address', a, a) + >>> sim.do(circuit) + >>> [sim.read_within_shot('target', a, out=int) for a in range(4)] + [0, 0, 0, 0] """ ``` @@ -1297,6 +1515,17 @@ def finish_circuit( self, ) -> kickmix.Circuit: """Returns the circuit built by the circuit builder. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> builder.x(km.q(0)) + >>> builder.cx(km.q(0), km.q(1)) + >>> builder.finish_circuit() + km.Circuit(''' + X q0 + CX q0 q1 + ''') """ ``` @@ -1574,6 +1803,16 @@ def free( arg: object, ) -> None: """Frees allocated qubits or bits, so that they can be allocated again. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> q = builder.alloc_qubits(2) + >>> builder.num_allocated_qubits + 2 + >>> builder.free(q) + >>> builder.num_allocated_qubits + 0 """ ``` @@ -2136,16 +2375,36 @@ def init_lookup( Looks up a value from a classical table, indexed by a quantum register. - Because this is an `init` method, the target qubits must start in the 0 state. - The target qubits will be reset before being used, transforming any bit errors - into probabilistic phase errors. + Because this is an `init` method, the target qubits must start in the 0 + state. The target qubits will be reset before being used, transforming + any bit errors into probabilistic phase errors. Args: table: The table of classical values, as a list of python integers. - The integers are interpreted in a little-endian fashion, meaning that - `table_data[address, out_index] == (table[address] >> out_index) & 1`. + The integers are interpreted in a little-endian fashion, meaning + that `table_data[address, out_index] == + (table[address] >> out_index) & 1`. address: The address used to select a value from the table. target: The clean qubits to store the output into. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> address = builder.create_quantum_register(2, name='address') + >>> target = builder.create_quantum_register(4, name='target') + >>> builder.init_lookup( + ... table=[2, 3, 5, 7], + ... address=address, + ... target=target, + ... ) + >>> circuit = builder.finish_circuit() + >>> sim = km.Simulator(batch_size=4) + >>> sim.use_same_registers_as(circuit) + >>> for a in range(4): + ... sim.write_within_shot('address', a, a) + >>> sim.do(circuit) + >>> [sim.read_within_shot('target', a, out=int) for a in range(4)] + [2, 3, 5, 7] """ ``` @@ -2675,22 +2934,16 @@ def set_max_qubits( Args: limit: The maximum number of qubits that can be allocated. Set to `float('inf')` to disable the limit. - """ -``` - -```python -# kickmix.CircuitBuilder.start_auto_free_scope - -# (in class kickmix.CircuitBuilder) -def start_auto_free_scope( - self, -) -> None: - """Starts a scope that automatically frees allocations. - - Call builder.end_auto_free_scope() to free allocations performed during the - scope. If multiple scopes are being used, they operate in LIFO order (i.e. like - a stack). + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> builder.set_max_qubits(4) + >>> builder.max_qubits + 4 + >>> q = builder.alloc_qubits(3) + >>> builder.num_free_qubits + 1 """ ``` @@ -3259,6 +3512,14 @@ def __init__( Args: batch_size: Determines how many simultaneous shots are being tracked by the simulator. + + Examples: + >>> import kickmix as km + >>> sim = km.Simulator(batch_size=4) + >>> sim.batch_size + 4 + >>> sim.read_across_shots(km.q(0)) + array([False, False, False, False]) """ ``` @@ -3324,6 +3585,28 @@ def do( Args: circuit: The circuit with instructions to apply. + + Examples: + >>> import kickmix as km + >>> import numpy as np + >>> sim = km.Simulator(batch_size=4) + >>> sim.write_across_shots(km.q(0), np.array([False, True, False, True])) + >>> sim.write_across_shots(km.q(1), np.array([False, False, True, True])) + + >>> sim.read_across_shots(km.q(0)) + array([False, True, False, True]) + >>> sim.read_across_shots(km.q(1)) + array([False, False, True, True]) + + >>> sim.do(km.Circuit(''' + ... CX q0 q1 + ... X q0 + ... ''')) + + >>> sim.read_across_shots(km.q(0)) + array([ True, False, True, False]) + >>> sim.read_across_shots(km.q(1)) + array([False, True, True, False]) """ ``` @@ -3362,6 +3645,16 @@ def read_phase_flipped_across_shots( Returns: The phase flip information. + + Examples: + >>> import kickmix as km + >>> sim = km.Simulator(batch_size=4) + >>> sim.write_across_shots(km.q(0), 0b1010) + >>> sim.do(km.Circuit('Z q0')) + >>> sim.read_phase_flipped_across_shots() + array([False, True, False, True]) + >>> sim.read_phase_flipped_across_shots(out=int) + 10 """ ``` @@ -3463,6 +3756,15 @@ def write_across_shots( Raises: IndexError: The given index isn't a qubit id or bit id. ValueError: The given new_value isn't valid. + + Examples: + >>> import kickmix as km + >>> sim = km.Simulator(batch_size=4) + >>> sim.write_across_shots(km.q(0), 0b1010) + >>> sim.read_across_shots(km.q(0)) + array([False, True, False, True]) + >>> sim.read_across_shots(km.q(0), out=int) + 10 """ ``` @@ -3830,6 +4132,14 @@ class array: km.array, rather than a normal python list of values, because a km.array's values are stored contiguously in memory alongside compressed type information. + + Examples: + >>> import kickmix as km + >>> arr = km.array([km.q(0), km.b(1), True]) + >>> arr + km.array([km.q(0), km.b(1), True]) + >>> len(arr) + 3 """ ``` @@ -3843,6 +4153,11 @@ def __add__( arg0: kickmix.array, ) -> kickmix.array: """Returns the concatenation of two arrays. + + Examples: + >>> import kickmix as km + >>> km.array([km.q(0)]) + km.array([km.b(1), True]) + km.array([km.q(0), km.b(1), True]) """ ``` @@ -3876,6 +4191,14 @@ def __getitem__( Returns: If the index is an int: the item at that index. If the index is a slice: a view of the array. + + Examples: + >>> import kickmix as km + >>> arr = km.array([km.q(0), km.q(1), km.q(2)]) + >>> arr[1] + km.q(1) + >>> arr[1:] + km.array([km.q(1), km.q(2)]) """ ``` @@ -3888,6 +4211,11 @@ def __len__( self, ) -> int: """Returns the length of the array. + + Examples: + >>> import kickmix as km + >>> len(km.array([km.q(0), km.q(1), km.q(2)])) + 3 """ ``` @@ -4302,6 +4630,12 @@ class xb: X basis values act like controls when given as the targets of bit flip operations. For example, cx(BitId(0), XBitId(1)) is equivalent to cz(BitId(0), BitId(1)). + + Examples: + >>> import kickmix as km + >>> x = km.xb(5) + >>> x.id + 5 """ ``` @@ -4431,6 +4765,37 @@ class xbool: X basis values act like controls when given as the targets of bit flip operations. For example, cx(a, xbool(b)) is equivalent to cz(a, b). + + Examples: + >>> import kickmix as km + >>> km.xbool(True) + km.xbool(True) + >>> bool(km.xbool(False)) + False + >>> bool(km.xbool(True)) + True + """ +``` + + +```python +# kickmix.xbool.__bool__ + +# (in class kickmix.xbool) +def __bool__( + self, +) -> bool: + """Converts the xbool into a bool. + + Returns: + True if the xbool represents a minus state, else False. + + Examples: + >>> import kickmix as km + >>> bool(km.xbool(True)) + True + >>> bool(km.xbool(False)) + False """ ``` diff --git a/kickmix/src/kickmix/id/qubit_or_bit_or_bool.h b/kickmix/src/kickmix/id/qubit_or_bit_or_bool.h index 5f41625a0..97bd09c7e 100644 --- a/kickmix/src/kickmix/id/qubit_or_bit_or_bool.h +++ b/kickmix/src/kickmix/id/qubit_or_bit_or_bool.h @@ -5,6 +5,7 @@ #include #include "bit_or_bool.h" +#include "qubit_or_bit.h" #include "qubit_or_true.h" namespace kickmix { @@ -27,6 +28,8 @@ struct QubitOrBitOrBool { } inline QubitOrBitOrBool(QubitOrTrue v) : tagged_id(v.tagged_id) { } + inline QubitOrBitOrBool(QubitOrBit v) : tagged_id(v.tagged_id) { + } inline QubitOrBitOrBool(BitOrBool v) : tagged_id(v.tagged_id) { } diff --git a/kickmix/src/kickmix/py/circuit/circuit.pybind.cc b/kickmix/src/kickmix/py/circuit/circuit.pybind.cc index 2efb38bd1..2846abd6c 100644 --- a/kickmix/src/kickmix/py/circuit/circuit.pybind.cc +++ b/kickmix/src/kickmix/py/circuit/circuit.pybind.cc @@ -52,16 +52,44 @@ void kickmix::register_circuit_methods(pybind11::class_ &c_circuit) { } return result; }, - "Returns the names and contents of registers in the circuit."); + clean_doc_string(R"DOC( + The names and contents of registers in the circuit, as a dict. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + + >>> circuit = km.Circuit(''' + ... REGISTER r0 "test" + ... APPEND_TO_REGISTER q0 r0 + ... APPEND_TO_REGISTER q1 r0 + ... APPEND_TO_REGISTER q2 r0 + ... REGISTER r2 "empty" + ... REGISTER r3 + ... APPEND_TO_REGISTER q3 r3 + ... APPEND_TO_REGISTER b0 r3 + ... CCX q0 q1 q2 + ... CX q0 q1 + ... X q0 + ... ''') + >>> data: dict[str, km.array] = circuit.register_data + >>> for k, v in data.items(): + ... print(f"{k!r}: {v!r}") + 'test': km.array([km.q(0), km.q(1), km.q(2)]) + km.r(1): km.array([]) + 'empty': km.array([]) + km.r(3): km.array([km.q(3), km.b(0)]) + )DOC") + .data()); c_circuit.def( "__repr__", [](const Circuit &self) -> std::string { - std::stringstream ss; - ss << "km.Circuit('''"; - if (self.num_ops > 0) { - ss << "\n "; + if (self.num_ops == 0 && self.register_data.empty()) { + return "km.Circuit()"; } + std::stringstream ss; + ss << "km.Circuit('''\n "; for (char c : self.str()) { ss << c; if (c == '\n') { @@ -78,28 +106,90 @@ void kickmix::register_circuit_methods(pybind11::class_ &c_circuit) { [](const Circuit &self) -> std::string { return self.text_diagram(); }, - "Returns a text diagram of the circuit."); + clean_doc_string(R"DOC( + @signature def text_diagram(self) -> str: + Returns a text diagram of the circuit. + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... X q0 + ... CX q0 q1 + ... CCX q0 q1 q2 + ... ''') + >>> print(circuit.text_diagram()) + q0: -X-@-@- + | | + q1: ---X-@- + | + q2: -----X- + )DOC") + .data()); c_circuit.def( "html_diagram", [](const Circuit &self) -> std::string { return self.html_diagram(); }, - "Returns an html diagram of the circuit."); + clean_doc_string(R"DOC( + @signature def html_diagram(self) -> str: + Returns an html diagram of the circuit. + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... X q0 + ... CX q0 q1 + ... ''') + >>> html = circuit.html_diagram() + >>> ' size_t { return self.max_magic(); }, - "Returns an upper bound on the number of CCX/CCZ gates run by the circuit."); + clean_doc_string(R"DOC( + @signature def max_magic(self) -> int: + Returns an upper bound on the number of CCX/CCZ gates run by the + circuit. + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... X q0 + ... CX q0 q1 + ... CCX q0 q1 q2 + ... CCZ q0 q1 q2 + ... ''') + >>> circuit.max_magic() + 2 + )DOC") + .data()); c_circuit.def( "reaction_depth", [](const Circuit &self) -> size_t { return self.reaction_depth(); }, - "Returns the reaction depth of the circuit, assuming it is powered by CCZ states."); + clean_doc_string(R"DOC( + @signature def reaction_depth(self) -> int: + Returns the reaction depth of the circuit, assuming it is powered by + CCZ states. + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... CCX q0 q1 q2 + ... CCX q2 q3 q4 + ... ''') + >>> circuit.reaction_depth() + 2 + )DOC") + .data()); c_circuit.def_property_readonly( "num_qubits", @@ -127,14 +217,66 @@ void kickmix::register_circuit_methods(pybind11::class_ &c_circuit) { return Circuit(text); }), pybind11::arg("circuit_text") = "", - "Creates a kickmix Circuit by parsing the given text."); + clean_doc_string(R"DOC( + Creates a circuit by parsing the given text as kickmix circuit file. + + For a definition of the expected text format, see the kickmix circuit file + format documentation at + https://github.com/quantumlib/Qualtran/blob/main/docs/kickmix/kickmix_format.md + + Roughly speaking, it's just a series of lines like "CCX q0 q1 q2". + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... X q0 + ... CX q0 q9 + ... ''') + + >>> simulator = km.Simulator(batch_size=1) + >>> print(simulator.read_within_shot(km.q(9), shot_index=0)) + False + >>> simulator.do(circuit) + >>> print(simulator.read_within_shot(km.q(9), shot_index=0)) + True + )DOC") + .data()); c_circuit.def( "__len__", [](const Circuit &self) -> size_t { return self.num_ops; }, - "Returns the number of instructions in the circuit."); + clean_doc_string(R"DOC( + Returns the number of instructions in the circuit. + + Caution: register data is not counted as instructions. + + Examples: + >>> import kickmix as km + >>> len(km.Circuit()) + 0 + + >>> len(km.Circuit(''' + ... X q0 + ... CX q0 q9 + ... ''')) + 2 + + >>> len(km.Circuit(''' + ... REGISTER r0 "test" + ... APPEND_TO_REGISTER q0 r0 + ... APPEND_TO_REGISTER q1 r0 + ... APPEND_TO_REGISTER q2 r0 + ... INFO_TAG "@enter:increment" + ... CCX q0 q1 q2 + ... CX q0 q1 + ... X q0 + ... INFO_TAG "@exit:increment" + ... ''')) + 5 + )DOC") + .data()); c_circuit.def_static( "from_file", @@ -363,8 +505,7 @@ void kickmix::register_circuit_methods(pybind11::class_ &c_circuit) { ''') >>> km.Circuit('CX q0 q1') * 0 - km.Circuit(''' - ''') + km.Circuit() >>> 5 * km.Circuit(''' ... REGISTER r0 "test" diff --git a/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.cc b/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.cc index e993a72c0..f2338975d 100644 --- a/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.cc +++ b/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.cc @@ -137,34 +137,15 @@ void kickmix_py::register_circuit_builder_methods(pybind11::class_>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> builder.x(km.q(0)) + >>> builder.finish_circuit() + km.Circuit(''' + X q0 + ''') )DOC") .data()); @@ -248,6 +229,16 @@ void kickmix_py::register_circuit_builder_methods(pybind11::class_>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> builder.set_max_qubits(4) + >>> builder.max_qubits + 4 + >>> q = builder.alloc_qubits(3) + >>> builder.num_free_qubits + 1 )DOC") .data()); @@ -274,27 +265,38 @@ void kickmix_py::register_circuit_builder_methods(pybind11::class_>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> q = builder.alloc_qubits(3) + >>> q + km.array([km.q(0), km.q(1), km.q(2)]) + >>> builder.num_allocated_qubits + 3 + )DOC") + .data()); c_circuit_builder.def( "alloc_bits", @@ -341,9 +343,20 @@ void kickmix_py::register_circuit_builder_methods(pybind11::class_>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> q = builder.alloc_qubits(2) + >>> builder.num_allocated_qubits + 2 + >>> builder.free(q) + >>> builder.num_allocated_qubits + 0 + )DOC") + .data()); c_circuit_builder.def( "create_quantum_register", @@ -368,19 +381,35 @@ void kickmix_py::register_circuit_builder_methods(pybind11::class_>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> reg = builder.create_quantum_register(2, name='data') + >>> reg + km.array([km.q(0), km.q(1)]) + >>> builder.x(reg[0]) + >>> builder.finish_circuit() + km.Circuit(''' + APPEND_TO_REGISTER q0 r0 + APPEND_TO_REGISTER q1 r0 + REGISTER r0 "data" + X q0 + ''') + )DOC") + .data()); c_circuit_builder.def( "create_classical_register", @@ -393,28 +422,56 @@ void kickmix_py::register_circuit_builder_methods(pybind11::class_>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> reg = builder.create_classical_register(2, name='meas') + >>> reg + km.array([km.b(0), km.b(1)]) + >>> builder.bit_store(reg[0], True) + >>> builder.finish_circuit() + km.Circuit(''' + APPEND_TO_REGISTER b0 r0 + APPEND_TO_REGISTER b1 r0 + REGISTER r0 "meas" + BIT_STORE1 b0 + ''') + )DOC") + .data()); c_circuit_builder.def( "finish_circuit", [](PyCircuitBuilder &self) -> Circuit { return self.builder.finish_circuit(); }, - R"DOC( + clean_doc_string(R"DOC( Returns the circuit built by the circuit builder. - )DOC"); + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> builder.x(km.q(0)) + >>> builder.cx(km.q(0), km.q(1)) + >>> builder.finish_circuit() + km.Circuit(''' + X q0 + CX q0 q1 + ''') + )DOC") + .data()); c_circuit_builder.def( "mark", @@ -692,24 +749,35 @@ void kickmix_py::register_circuit_builder_methods(pybind11::class_ None: Appends many CZ gates to the circuit. - This method broadcasts over km.array arguments. If any of the arguments is a - km.array, an append is performed for each array index. When multiple km.array - arguments are used, they are combined by zipping (i.e. using two arrays of - length n will produce n instructions rather than n*n instructions). + This method broadcasts over km.array arguments. If any of the arguments + is a km.array, an append is performed for each array index. When + multiple km.array arguments are used, they are combined by zipping + (i.e. using two arrays of length n will produce n instructions rather + than n*n instructions). - The exact kind of operation(s) appended to the circuit depends on the types of - the controls. For example, if one of the controls is the boolean value True - instead of a qubit, then a Z instruction will be appended instead of a CZ - instruction. + The exact kind of operation(s) appended to the circuit depends on the + types of the controls. For example, if one of the controls is the + boolean value True instead of a qubit, then a Z instruction will be + appended instead of a CZ instruction. Args: control1: One of the controls determining if phasing occurs. control2: The other controls determining if phasing occurs. - )DOC"); + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> builder.cz(km.q(0), km.q(1)) + >>> builder.finish_circuit() + km.Circuit(''' + CZ q0 q1 + ''') + )DOC") + .data()); c_circuit_builder.def( "swap", @@ -835,23 +903,44 @@ void kickmix_py::register_circuit_builder_methods(pybind11::class_ None: Appends operations to perform `let target := table[address]`. Looks up a value from a classical table, indexed by a quantum register. - Because this is an `init` method, the target qubits must start in the 0 state. - The target qubits will be reset before being used, transforming any bit errors - into probabilistic phase errors. + Because this is an `init` method, the target qubits must start in the 0 + state. The target qubits will be reset before being used, transforming + any bit errors into probabilistic phase errors. Args: table: The table of classical values, as a list of python integers. - The integers are interpreted in a little-endian fashion, meaning that - `table_data[address, out_index] == (table[address] >> out_index) & 1`. + The integers are interpreted in a little-endian fashion, meaning + that `table_data[address, out_index] == + (table[address] >> out_index) & 1`. address: The address used to select a value from the table. target: The clean qubits to store the output into. - )DOC"); + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> address = builder.create_quantum_register(2, name='address') + >>> target = builder.create_quantum_register(4, name='target') + >>> builder.init_lookup( + ... table=[2, 3, 5, 7], + ... address=address, + ... target=target, + ... ) + >>> circuit = builder.finish_circuit() + >>> sim = km.Simulator(batch_size=4) + >>> sim.use_same_registers_as(circuit) + >>> for a in range(4): + ... sim.write_within_shot('address', a, a) + >>> sim.do(circuit) + >>> [sim.read_within_shot('target', a, out=int) for a in range(4)] + [2, 3, 5, 7] + )DOC") + .data()); c_circuit_builder.def( "del_lookup", @@ -860,23 +949,43 @@ void kickmix_py::register_circuit_builder_methods(pybind11::class_ None: Appends operations to perform `del target := table[address]`. - Clears a value created by looking up a value from a classical table, indexed by - a quantum register. + Clears a value created by looking up a value from a classical table, + indexed by a quantum register. - Because this is a `del` method, the target qubits will end in the 0 state. + Because this is a `del` method, the target qubits will end in the 0 + state. Args: table: The table of classical values, as a list of python integers. - The integers are interpreted in a little-endian fashion, meaning that - `table_data[address, out_index] == (table[address] >> out_index) & 1`. + The integers are interpreted in a little-endian fashion, meaning + that `table_data[address, out_index] == + (table[address] >> out_index) & 1`. address: The address used to select a value from the table. Qubits are in little-endian order. target: The qubits to clear the lookup result from. - )DOC"); + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + >>> address = builder.create_quantum_register(2, name='address') + >>> target = builder.create_quantum_register(4, name='target') + >>> table = [2, 3, 5, 7] + >>> builder.init_lookup(table=table, address=address, target=target) + >>> builder.del_lookup(table=table, address=address, target=target) + >>> circuit = builder.finish_circuit() + >>> sim = km.Simulator(batch_size=4) + >>> sim.use_same_registers_as(circuit) + >>> for a in range(4): + ... sim.write_within_shot('address', a, a) + >>> sim.do(circuit) + >>> [sim.read_within_shot('target', a, out=int) for a in range(4)] + [0, 0, 0, 0] + )DOC") + .data()); c_circuit_builder.def( "iadd", diff --git a/kickmix/src/kickmix/py/circuit/circuit_test.py b/kickmix/src/kickmix/py/circuit/circuit_test.py index 9c9ae3f8a..23dd10b07 100644 --- a/kickmix/src/kickmix/py/circuit/circuit_test.py +++ b/kickmix/src/kickmix/py/circuit/circuit_test.py @@ -17,6 +17,30 @@ import kickmix as km +def test_repr(): + assert repr(km.Circuit()) == """km.Circuit()""" + + assert ( + repr(km.Circuit(''' + REGISTER r0 "test" + ''')) + == """km.Circuit(''' + REGISTER r0 "test" +''')""" + ) + + assert ( + repr(km.Circuit(''' + CCX q0 q1 q2 + CZ q0 q1 + ''')) + == """km.Circuit(''' + CCX q0 q1 q2 + CZ q0 q1 +''')""" + ) + + def test_add(): a = km.Circuit(''' CCX q0 q1 q2 diff --git a/kickmix/src/kickmix/py/interactive_simulator.pybind.cc b/kickmix/src/kickmix/py/interactive_simulator.pybind.cc index b9c17dd70..02c0c848f 100644 --- a/kickmix/src/kickmix/py/interactive_simulator.pybind.cc +++ b/kickmix/src/kickmix/py/interactive_simulator.pybind.cc @@ -402,6 +402,7 @@ void write_across_shots_1d_array(PyInteractiveSimulator &self, QubitOrBit index, } auto stride = array.strides(0); auto in_ptr = array.data(0); + self.ensure_big_enough_state_for(index); for (size_t k = 0; k < self.batch_size; k++) { set_single_bit(self, index, k, *in_ptr); in_ptr += stride; @@ -420,6 +421,9 @@ void write_across_shots_2d_int( throw std::invalid_argument(ss.str()); } + for (auto q : indices) { + self.ensure_big_enough_state_for(q); + } size_t k = 0; for (const auto &item : value_obj) { write_across_shots_1d_int(self, indices[k++], pybind11::cast(item)); @@ -681,6 +685,14 @@ void kickmix_py::register_interactive_simulator_methods(pybind11::class_>> import kickmix as km + >>> sim = km.Simulator(batch_size=4) + >>> sim.batch_size + 4 + >>> sim.read_across_shots(km.q(0)) + array([False, False, False, False]) )DOC") .data()); @@ -703,6 +715,28 @@ void kickmix_py::register_interactive_simulator_methods(pybind11::class_>> import kickmix as km + >>> import numpy as np + >>> sim = km.Simulator(batch_size=4) + >>> sim.write_across_shots(km.q(0), np.array([False, True, False, True])) + >>> sim.write_across_shots(km.q(1), np.array([False, False, True, True])) + + >>> sim.read_across_shots(km.q(0)) + array([False, True, False, True]) + >>> sim.read_across_shots(km.q(1)) + array([False, False, True, True]) + + >>> sim.do(km.Circuit(''' + ... CX q0 q1 + ... X q0 + ... ''')) + + >>> sim.read_across_shots(km.q(0)) + array([ True, False, True, False]) + >>> sim.read_across_shots(km.q(1)) + array([False, True, True, False]) )DOC") .data()); @@ -840,6 +874,16 @@ void kickmix_py::register_interactive_simulator_methods(pybind11::class_>> import kickmix as km + >>> sim = km.Simulator(batch_size=4) + >>> sim.write_across_shots(km.q(0), 0b1010) + >>> sim.do(km.Circuit('Z q0')) + >>> sim.read_phase_flipped_across_shots() + array([False, True, False, True]) + >>> sim.read_phase_flipped_across_shots(out=int) + 10 )DOC") .data()); @@ -910,6 +954,15 @@ void kickmix_py::register_interactive_simulator_methods(pybind11::class_>> import kickmix as km + >>> sim = km.Simulator(batch_size=4) + >>> sim.write_across_shots(km.q(0), 0b1010) + >>> sim.read_across_shots(km.q(0)) + array([False, True, False, True]) + >>> sim.read_across_shots(km.q(0), out=int) + 10 )DOC") .data()); diff --git a/kickmix/src/kickmix/py/main.pybind.cc b/kickmix/src/kickmix/py/main.pybind.cc index 88b132c8b..bca68c15c 100644 --- a/kickmix/src/kickmix/py/main.pybind.cc +++ b/kickmix/src/kickmix/py/main.pybind.cc @@ -33,7 +33,21 @@ PYBIND11_MODULE(kickmix, m) { m, "Circuit", clean_doc_string(R"DOC( - A kickmix circuit. + An immutable kickmix circuit. + + Examples: + >>> import kickmix as km + >>> circuit = km.Circuit(''' + ... X q0 + ... CX q0 q9 + ... ''') + + >>> simulator = km.Simulator(batch_size=1) + >>> print(simulator.read_within_shot(km.q(9), shot_index=0)) + False + >>> simulator.do(circuit) + >>> print(simulator.read_within_shot(km.q(9), shot_index=0)) + True )DOC") .data()); @@ -41,7 +55,35 @@ PYBIND11_MODULE(kickmix, m) { m, "CircuitBuilder", clean_doc_string(R"DOC( - A kickmix circuit builder. + A builder class that helps create a kickmix circuit. + + Examples: + >>> import kickmix as km + >>> builder = km.CircuitBuilder() + + >>> target = builder.create_quantum_register(7, "target") + >>> shift = builder.create_quantum_register(3, "shift") + >>> builder.left_rotate(target, shift) + >>> print(builder.finish_circuit().text_diagram()) + q0: -target[0]-@-X-@---------@-X-@-----------@---X---@-----@-X-@----------- + | | | | | | | | | | | | + q1: -target[1]-X-@-X---------|-|-|-----------|-@-|-X-|-@---X-@-X----------- + | | | | | | | | | | | + q2: -target[2]---|-@---X---@-X-@-X-----------|-|-|-|-|-|-----|-@---X---@--- + | | | | | | | | | | | | | | | + q3: -target[3]---|-|-@-|-X-|-@-|-@---X---@---|-X-|-@-|-X-----|-|-@-|-X-|-@- + | | | | | | | | | | | | | | | | | | | | | | + q4: -target[4]---|-|-|-|-|-|-|-|-|-@-|-X-|-@-X---@-|-X-------|-|-|-|-|-|-|- + | | | | | | | | | | | | | | | | | | | | | | | + q5: -target[5]---|-|-X-|-@-|-X-|-|-X-|-@-|-X-----|-|---@-X-@-|-|-X-|-@-|-X- + | | | | | | | | | | | | | | | | | | | | + q6: -target[6]---|-X---@-|-X---|-X---@-|-X-------|-|---X-@-X-|-X---@-|-X--- + | | | | | | | | | | | | + q7: -shift[0]----@-----@-@---@-|-----|-|---@-----|-|-----|---|-----|-|----- + | | | | | | | | | | | + q8: -shift[1]----------------X-@-----@-@---X-@---|-|-----|-@-|-----|-|----- + | | | | | | | | + q9: -shift[2]--------------------------------X---@-@-----@-X-@-----@-@----- )DOC") .data()); @@ -55,6 +97,14 @@ PYBIND11_MODULE(kickmix, m) { km.array, rather than a normal python list of values, because a km.array's values are stored contiguously in memory alongside compressed type information. + + Examples: + >>> import kickmix as km + >>> arr = km.array([km.q(0), km.b(1), True]) + >>> arr + km.array([km.q(0), km.b(1), True]) + >>> len(arr) + 3 )DOC") .data()); @@ -91,6 +141,12 @@ PYBIND11_MODULE(kickmix, m) { X basis values act like controls when given as the targets of bit flip operations. For example, cx(BitId(0), XBitId(1)) is equivalent to cz(BitId(0), BitId(1)). + + Examples: + >>> import kickmix as km + >>> x = km.xb(5) + >>> x.id + 5 )DOC") .data()); @@ -106,6 +162,15 @@ PYBIND11_MODULE(kickmix, m) { X basis values act like controls when given as the targets of bit flip operations. For example, cx(a, xbool(b)) is equivalent to cz(a, b). + + Examples: + >>> import kickmix as km + >>> km.xbool(True) + km.xbool(True) + >>> bool(km.xbool(False)) + False + >>> bool(km.xbool(True)) + True )DOC") .data()); @@ -121,7 +186,6 @@ PYBIND11_MODULE(kickmix, m) { PYBIND_TYPE_PTR_XBIT_ID = pybind11::type::of().ptr(); PYBIND_TYPE_PTR_XBOOL = pybind11::type::of().ptr(); PYBIND_TYPE_PTR_REGISTER_ID = pybind11::type::of().ptr(); - ; register_circuit_methods(c_circuit); register_circuit_builder_mark_class(m); diff --git a/kickmix/src/kickmix/py/val/array.pybind.cc b/kickmix/src/kickmix/py/val/array.pybind.cc index 5db82bb25..160060d8f 100644 --- a/kickmix/src/kickmix/py/val/array.pybind.cc +++ b/kickmix/src/kickmix/py/val/array.pybind.cc @@ -179,6 +179,14 @@ void kickmix_py::register_qcarray_methods(pybind11::class_ &c_qcarray Returns: If the index is an int: the item at that index. If the index is a slice: a view of the array. + + Examples: + >>> import kickmix as km + >>> arr = km.array([km.q(0), km.q(1), km.q(2)]) + >>> arr[1] + km.q(1) + >>> arr[1:] + km.array([km.q(1), km.q(2)]) )DOC") .data()); @@ -189,6 +197,11 @@ void kickmix_py::register_qcarray_methods(pybind11::class_ &c_qcarray }, clean_doc_string(R"DOC( Returns the length of the array. + + Examples: + >>> import kickmix as km + >>> len(km.array([km.q(0), km.q(1), km.q(2)])) + 3 )DOC") .data()); @@ -202,6 +215,11 @@ void kickmix_py::register_qcarray_methods(pybind11::class_ &c_qcarray }, clean_doc_string(R"DOC( Returns the concatenation of two arrays. + + Examples: + >>> import kickmix as km + >>> km.array([km.q(0)]) + km.array([km.b(1), True]) + km.array([km.q(0), km.b(1), True]) )DOC") .data()); diff --git a/kickmix/src/kickmix/py/val/id.pybind.cc b/kickmix/src/kickmix/py/val/id.pybind.cc index a2d93e324..e7cda466a 100644 --- a/kickmix/src/kickmix/py/val/id.pybind.cc +++ b/kickmix/src/kickmix/py/val/id.pybind.cc @@ -557,6 +557,26 @@ void kickmix_py::register_xbool_methods(pybind11::class_ &c_xbool) { )DOC") .data()); + c_xbool.def( + "__bool__", + [](XBool &self) -> bool { + return self.is_minus_ket(); + }, + clean_doc_string(R"DOC( + Converts the xbool into a bool. + + Returns: + True if the xbool represents a minus state, else False. + + Examples: + >>> import kickmix as km + >>> bool(km.xbool(True)) + True + >>> bool(km.xbool(False)) + False + )DOC") + .data()); + c_xbool.def( "__repr__", [](XBool &self) -> std::string_view { From 7a7ccd8954ec692c2247434f9f0c4ecfe8b2e0af Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Wed, 14 Oct 2026 21:18:38 -0700 Subject: [PATCH 2/2] address comments --- docs/kickmix/api_reference.md | 54 ++++++++++--------- .../src/kickmix/py/circuit/circuit.pybind.cc | 1 - .../py/circuit/circuit_builder.pybind.cc | 15 ++++-- .../py/circuit/circuit_builder.pybind.h | 40 -------------- kickmix/src/kickmix/py/main.pybind.cc | 38 +++++++------ 5 files changed, 58 insertions(+), 90 deletions(-) diff --git a/docs/kickmix/api_reference.md b/docs/kickmix/api_reference.md index 9b74fd24c..e2e8b95f7 100644 --- a/docs/kickmix/api_reference.md +++ b/docs/kickmix/api_reference.md @@ -712,7 +712,6 @@ def register_data( Examples: >>> import kickmix as km - >>> builder = km.CircuitBuilder() >>> circuit = km.Circuit(''' ... REGISTER r0 "test" @@ -816,28 +815,26 @@ class CircuitBuilder: >>> builder = km.CircuitBuilder() >>> target = builder.create_quantum_register(7, "target") - >>> shift = builder.create_quantum_register(3, "shift") + >>> shift = builder.create_quantum_register(2, "shift") >>> builder.left_rotate(target, shift) >>> print(builder.finish_circuit().text_diagram()) - q0: -target[0]-@-X-@---------@-X-@-----------@---X---@-----@-X-@----------- - | | | | | | | | | | | | - q1: -target[1]-X-@-X---------|-|-|-----------|-@-|-X-|-@---X-@-X----------- - | | | | | | | | | | | - q2: -target[2]---|-@---X---@-X-@-X-----------|-|-|-|-|-|-----|-@---X---@--- - | | | | | | | | | | | | | | | - q3: -target[3]---|-|-@-|-X-|-@-|-@---X---@---|-X-|-@-|-X-----|-|-@-|-X-|-@- - | | | | | | | | | | | | | | | | | | | | | | - q4: -target[4]---|-|-|-|-|-|-|-|-|-@-|-X-|-@-X---@-|-X-------|-|-|-|-|-|-|- - | | | | | | | | | | | | | | | | | | | | | | | - q5: -target[5]---|-|-X-|-@-|-X-|-|-X-|-@-|-X-----|-|---@-X-@-|-|-X-|-@-|-X- - | | | | | | | | | | | | | | | | | | | | - q6: -target[6]---|-X---@-|-X---|-X---@-|-X-------|-|---X-@-X-|-X---@-|-X--- - | | | | | | | | | | | | - q7: -shift[0]----@-----@-@---@-|-----|-|---@-----|-|-----|---|-----|-|----- - | | | | | | | | | | | - q8: -shift[1]----------------X-@-----@-@---X-@---|-|-----|-@-|-----|-|----- - | | | | | | | | - q9: -shift[2]--------------------------------X---@-@-----@-X-@-----@-@----- + q0: -target[0]-SWAP-------------SWAP-------------SWAP----------- + | | | + q1: -target[1]-SWAP-------------|----------------|----SWAP------ + | | | | + q2: -target[2]-|----SWAP--------SWAP-------------|----|--------- + | | | | | + q3: -target[3]-|----|----SWAP---|----SWAP--------|----SWAP------ + | | | | | | | + q4: -target[4]-|----|----|------|----|----SWAP---SWAP-|--------- + | | | | | | | | + q5: -target[5]-|----|----SWAP---|----|----SWAP---|----|----SWAP- + | | | | | | | | | + q6: -target[6]-|----SWAP-|------|----SWAP-|------|----|----SWAP- + | | | | | | | | | + q7: -shift[0]--@----@----@----@-|----|----|----@-|----|----|---- + | | | | | | | | + q8: -shift[1]-----------------X-@----@----@----X-@----@----@---- """ ``` @@ -884,12 +881,14 @@ def alloc_qubits( ) -> kickmix.array: """Allocates clean qubits to use as workspace in a circuit. - Allocated qubits can be deallocated by using the `CircuitBuilder.free` - method. Qubits allocated within a `with builder.scope(auto_free=True):` - block are deallocated automatically upon execution exiting the block. + Allocated qubits can be deallocated with the `km.CircuitBuilder.free` method. + + If the allocation would push `km.CircuitBuilder.num_allocate_qubits` above + `km.CircuitBuilder.max_qubits` (which defaults to infinity), it will raise + an exception instead of returning a result. Although the qubits returned by this method *should* be clean, nothing - *enforces* this. If a prior user of the allocated qubit failed to + *enforces* this. If a prior user of the allocated qubits failed to correctly clean some qubits before freeing them, the returned qubits may contain their buggy garbage. As such, it's recommended that you reset the qubits before using them. Reset gates also have the nice side effect @@ -903,6 +902,11 @@ def alloc_qubits( An array containing the ids of the allocated qubits. Until these ids are freed, they won't be returned by any other allocation calls. + Raises: + ValueError: + Attempted to allocate more qubits than the maximum (if set). + See also `km.CircuitBuilder.num_free_qubits`. + Examples: >>> import kickmix as km >>> builder = km.CircuitBuilder() diff --git a/kickmix/src/kickmix/py/circuit/circuit.pybind.cc b/kickmix/src/kickmix/py/circuit/circuit.pybind.cc index 2846abd6c..7576fef0d 100644 --- a/kickmix/src/kickmix/py/circuit/circuit.pybind.cc +++ b/kickmix/src/kickmix/py/circuit/circuit.pybind.cc @@ -57,7 +57,6 @@ void kickmix::register_circuit_methods(pybind11::class_ &c_circuit) { Examples: >>> import kickmix as km - >>> builder = km.CircuitBuilder() >>> circuit = km.Circuit(''' ... REGISTER r0 "test" diff --git a/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.cc b/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.cc index ecd481e67..d591c726c 100644 --- a/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.cc +++ b/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.cc @@ -268,12 +268,14 @@ void kickmix_py::register_circuit_builder_methods(pybind11::class_>> import kickmix as km >>> builder = km.CircuitBuilder() diff --git a/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.h b/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.h index 8c62f8892..2ed9da255 100644 --- a/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.h +++ b/kickmix/src/kickmix/py/circuit/circuit_builder.pybind.h @@ -1,7 +1,6 @@ #ifndef KICKGEN_PYBIND_CIRCUIT_BUILDER_H #define KICKGEN_PYBIND_CIRCUIT_BUILDER_H -#include #include #include #include @@ -23,8 +22,6 @@ struct PyCircuitBuilder { size_t max_qubits = SIZE_MAX; size_t allocated_qubits = 0; size_t allocated_bits = 0; - std::vector> scope_qubit_allocations; - std::vector> scope_bit_allocations; /// Backing storage for the `std::string_view` names referenced by `builder.marks`. std::unordered_set mark_names; @@ -35,37 +32,6 @@ struct PyCircuitBuilder { PyCircuitBuilder(PyCircuitBuilder &&) = default; PyCircuitBuilder &operator=(PyCircuitBuilder &&) = default; - void start_auto_free_scope() { - scope_qubit_allocations.push_back({}); - scope_bit_allocations.push_back({}); - } - void end_auto_free_scope() { - if (!scope_qubit_allocations.empty()) { - while (!scope_qubit_allocations.back().empty()) { - uint32_t id = scope_qubit_allocations.back().back(); - scope_qubit_allocations.back().pop_back(); - if (id < qubit_allocations.size() && qubit_allocations[id]) { - free_qids.push_back(kickmix::QubitId{id}); - qubit_allocations[id] = false; - } - allocated_qubits -= 1; - } - scope_qubit_allocations.pop_back(); - } - if (!scope_bit_allocations.empty()) { - while (!scope_bit_allocations.back().empty()) { - uint32_t id = scope_bit_allocations.back().back(); - scope_bit_allocations.back().pop_back(); - if (id < bit_allocations.size() && bit_allocations[id]) { - free_qids.push_back(kickmix::QubitId{id}); - bit_allocations[id] = false; - } - allocated_bits -= 1; - } - scope_bit_allocations.pop_back(); - } - } - kickmix::QubitId alloc_qubit() { kickmix::QubitId result; if (free_qids.empty()) { @@ -78,9 +44,6 @@ struct PyCircuitBuilder { qubit_allocations.push_back(false); } qubit_allocations[result.untagged_id()] = true; - if (!scope_qubit_allocations.empty()) { - scope_qubit_allocations.back().push_back(result.untagged_id()); - } allocated_qubits += 1; return result; } @@ -97,9 +60,6 @@ struct PyCircuitBuilder { bit_allocations.push_back(false); } bit_allocations[result.untagged_id()] = true; - if (!scope_bit_allocations.empty()) { - scope_bit_allocations.back().push_back(result.untagged_id()); - } allocated_bits += 1; return result; } diff --git a/kickmix/src/kickmix/py/main.pybind.cc b/kickmix/src/kickmix/py/main.pybind.cc index bca68c15c..7466642f7 100644 --- a/kickmix/src/kickmix/py/main.pybind.cc +++ b/kickmix/src/kickmix/py/main.pybind.cc @@ -62,28 +62,26 @@ PYBIND11_MODULE(kickmix, m) { >>> builder = km.CircuitBuilder() >>> target = builder.create_quantum_register(7, "target") - >>> shift = builder.create_quantum_register(3, "shift") + >>> shift = builder.create_quantum_register(2, "shift") >>> builder.left_rotate(target, shift) >>> print(builder.finish_circuit().text_diagram()) - q0: -target[0]-@-X-@---------@-X-@-----------@---X---@-----@-X-@----------- - | | | | | | | | | | | | - q1: -target[1]-X-@-X---------|-|-|-----------|-@-|-X-|-@---X-@-X----------- - | | | | | | | | | | | - q2: -target[2]---|-@---X---@-X-@-X-----------|-|-|-|-|-|-----|-@---X---@--- - | | | | | | | | | | | | | | | - q3: -target[3]---|-|-@-|-X-|-@-|-@---X---@---|-X-|-@-|-X-----|-|-@-|-X-|-@- - | | | | | | | | | | | | | | | | | | | | | | - q4: -target[4]---|-|-|-|-|-|-|-|-|-@-|-X-|-@-X---@-|-X-------|-|-|-|-|-|-|- - | | | | | | | | | | | | | | | | | | | | | | | - q5: -target[5]---|-|-X-|-@-|-X-|-|-X-|-@-|-X-----|-|---@-X-@-|-|-X-|-@-|-X- - | | | | | | | | | | | | | | | | | | | | - q6: -target[6]---|-X---@-|-X---|-X---@-|-X-------|-|---X-@-X-|-X---@-|-X--- - | | | | | | | | | | | | - q7: -shift[0]----@-----@-@---@-|-----|-|---@-----|-|-----|---|-----|-|----- - | | | | | | | | | | | - q8: -shift[1]----------------X-@-----@-@---X-@---|-|-----|-@-|-----|-|----- - | | | | | | | | - q9: -shift[2]--------------------------------X---@-@-----@-X-@-----@-@----- + q0: -target[0]-SWAP-------------SWAP-------------SWAP----------- + | | | + q1: -target[1]-SWAP-------------|----------------|----SWAP------ + | | | | + q2: -target[2]-|----SWAP--------SWAP-------------|----|--------- + | | | | | + q3: -target[3]-|----|----SWAP---|----SWAP--------|----SWAP------ + | | | | | | | + q4: -target[4]-|----|----|------|----|----SWAP---SWAP-|--------- + | | | | | | | | + q5: -target[5]-|----|----SWAP---|----|----SWAP---|----|----SWAP- + | | | | | | | | | + q6: -target[6]-|----SWAP-|------|----SWAP-|------|----|----SWAP- + | | | | | | | | | + q7: -shift[0]--@----@----@----@-|----|----|----@-|----|----|---- + | | | | | | | | + q8: -shift[1]-----------------X-@----@----@----X-@----@----@---- )DOC") .data());