Skip to content
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added

- The cycle time [seconds] can be set when instantiating the `QuantifySchedulerExporter` through the `cycle_time`
parameter.
- `CircuitBuilder` accepts multiple (qu)bit registers through `add_register` method.
parameter
- `CircuitBuilder` accepts multiple (qu)bit registers through `add_register` method
- Docstrings added to (user facing) public methods
- Add `interaction_graph` as a property of the `Circuit`.

## [ 0.9.0 ] - [ 2025-12-19 ]
Expand Down
12 changes: 11 additions & 1 deletion mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ plugins:
- scripts/gen_reference_page.py
- literate-nav:
nav_file: reference.md
- mkdocstrings
- autorefs
- mkdocstrings:
handlers:
python:
options:
docstring_style: google
show_bases: true
show_signature: true
separate_signature: true
show_signature_annotations: true
- glightbox
- search

128 changes: 85 additions & 43 deletions opensquirrel/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,59 +29,54 @@
class Circuit:
"""The Circuit class is the only interface to access OpenSquirrel's features.

Examples:
>>> c = Circuit.from_string("version 3.0; qubit[3] q; h q[0]")
>>> c
Example:
```python
>>> circuit = Circuit.from_string("version 3.0; qubit[3] q; h q[0]")
>>> circuit
```
```
version 3.0
<BLANKLINE>

qubit[3] q
<BLANKLINE>

h q[0]
<BLANKLINE>
>>> c.decomposer(decomposer=mckay_decomposer.McKayDecomposer)
>>> c
```
```python
>>> circuit.decompose(decomposer=McKayDecomposer())
>>> circuit
```
```
version 3.0
<BLANKLINE>

qubit[3] q
<BLANKLINE>

x90 q[0]
rz q[0], 1.5707963
x90 q[0]
<BLANKLINE>

```

"""

def __init__(self, register_manager: RegisterManager, ir: IR) -> None:
"""Create a circuit object from a register manager and an IR."""
self.register_manager = register_manager
self.ir = ir

def __repr__(self) -> str:
"""Write the circuit to a cQASM 3 string."""
from opensquirrel.writer import writer

return writer.circuit_to_string(self)

def __eq__(self, other: Any) -> bool:
if not isinstance(other, Circuit):
return False
return self.register_manager == other.register_manager and self.ir == other.ir

@classmethod
def from_string(cls, cqasm3_string: str) -> Circuit:
"""Create a circuit object from a cQasm3 string. All the gates in the circuit need to be defined in
the `gates` argument.

* type-checking is performed, eliminating qubit indices errors and incoherences
* checks that used gates are supported and mentioned in `gates` with appropriate signatures
* does not support map or variables, and other things...
* for example of `gates` dictionary, please look at TestGates.py
def from_string(cls, cqasm_string: str) -> Circuit:
"""Create a circuit from a [cQASM](https://qutech-delft.github.io/cQASM-spec/) string.

Args:
cqasm3_string: a cQASM 3 string
cqasm_string (str): A cQASM string.

Returns:
Circuit: The circuit generated from the cQASM string.

"""
from opensquirrel.reader import LibQasmParser

return LibQasmParser().circuit_from_string(cqasm3_string)
return LibQasmParser().circuit_from_string(cqasm_string)

@property
def qubit_register_size(self) -> int:
Expand Down Expand Up @@ -135,6 +130,15 @@ def interaction_graph(self) -> InteractionGraph:
return graph

def asm_filter(self, backend_name: str) -> None:
"""Filter the assembly declarations in the circuit for a specific backend.

Note:
This will remove all assembly declarations that do not match the specified backend name.

Args:
backend_name (str): The backend name to filter for.

"""
self.ir.statements = [
statement
for statement in self.ir.statements
Expand All @@ -143,23 +147,32 @@ def asm_filter(self, backend_name: str) -> None:
]

def decompose(self, decomposer: Decomposer) -> None:
"""Generic decomposition pass.
It applies the given decomposer function to every gate in the circuit.
"""Decomposes the circuit using to the specified decomposer.

Args:
decomposer (Decomposer): The decomposer to apply.

"""
from opensquirrel.passes.decomposer import general_decomposer

general_decomposer.decompose(self.ir, decomposer)

def export(self, exporter: Exporter) -> Any:
"""Generic export pass.
Exports the circuit using the specified exporter.
"""Exports the circuit using the specified exporter.

Args:
exporter (Exporter): The exporter to apply.

"""
return exporter.export(self)

def map(self, mapper: Mapper) -> None:
"""Generic qubit mapper pass.
Map the (virtual) qubits of the circuit to the physical qubits of the target hardware.
"""Maps the (virtual) qubits of the circuit to the physical qubits of the target hardware
using the specified mapper.

Args:
mapper (Mapper): The mapper to apply.

"""
from opensquirrel.passes.mapper.qubit_remapper import remap_ir

Expand All @@ -168,22 +181,51 @@ def map(self, mapper: Mapper) -> None:
remap_ir(self, mapping)

def merge(self, merger: Merger) -> None:
"""Generic merge pass. It applies the given merger to the circuit."""
"""Merges the circuit using the specified merger.

Args:
merger (Merger): The merger to apply.

"""
merger.merge(self.ir, self.qubit_register_size)

def route(self, router: Router) -> None:
"""Generic router pass. It applies the given router to the circuit."""
"""Routes the circuit using the specified router.

Args:
router (Router): The router to apply.

"""
router.route(self.ir, self.qubit_register_size)

def replace(self, gate: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None:
"""Manually replace occurrences of a given gate with a list of gates.
`replacement_gates_function` is a callable that takes the arguments of the gate that is to be replaced and
returns the decomposition as a list of gates.

Args:
gate (type[Gate]): The gate type to be replaced.
replacement_gates_function (Callable[..., list[Gate]]): function that describes the replacement gates.

"""
from opensquirrel.passes.decomposer import general_decomposer

general_decomposer.replace(self.ir, gate, replacement_gates_function)

def validate(self, validator: Validator) -> None:
"""Generic validator pass. It applies the given validator to the circuit."""
"""Validates the circuit using the specified validator.

Args:
validator (Validator): The validator to apply.

"""
validator.validate(self.ir)

def __eq__(self, other: Any) -> bool:
if not isinstance(other, Circuit):
return False
return self.register_manager == other.register_manager and self.ir == other.ir

def __repr__(self) -> str:
"""Write the circuit to a cQASM 3 string."""
from opensquirrel.writer import writer

return writer.circuit_to_string(self)
30 changes: 22 additions & 8 deletions opensquirrel/circuit_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,28 @@
class CircuitBuilder:
"""
A class using the builder pattern to make construction of circuits easy from Python.
Adds corresponding instruction when a method is called. Checks that instructions are known and called with the right
arguments.
Adds corresponding instruction when a method is called. Checks that instructions are known and
called with the right arguments.
Mainly here to allow for Qiskit-style circuit construction:

Args:
qubit_register_size (int): Size of the qubit register
bit_register_size (int): Size of the bit register

Example:
>>> CircuitBuilder(qubit_register_size=3, bit_register_size=3).\
H(0).CNOT(0, 1).CNOT(0, 2).\
to_circuit()
```python
>>> CircuitBuilder(qubit_register_size=3, bit_register_size=3).H(0).CNOT(0, 1).CNOT(0, 2).to_circuit()
```
```
version 3.0
<BLANKLINE>

qubit[3] q
<BLANKLINE>

h q[0]
cnot q[0], q[1]
cnot q[0], q[2]
<BLANKLINE>

```
"""

def __init__(
Expand Down Expand Up @@ -77,6 +79,12 @@ def __getattr__(self, attr: str) -> Any:
return self.__getattribute__(attr)

def add_register(self, register: QubitRegister | BitRegister) -> None:
"""Add a (qu)bit register to the circuit builder.

Args:
register (QubitRegister | BitRegister): (Qu)bit register to add.

"""
self.register_manager.add_register(register)

def _check_qubit_out_of_bounds_access(self, qubit: QubitLike) -> None:
Expand Down Expand Up @@ -135,4 +143,10 @@ def _add_statement(self, attr: str, *args: Any) -> Self:
return self

def to_circuit(self) -> Circuit:
"""Build the circuit.

Returns:
Circuit: The built circuit.

"""
return Circuit(deepcopy(self.register_manager), deepcopy(self.ir))
7 changes: 5 additions & 2 deletions opensquirrel/circuit_matrix_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ def visit_gate(self, gate: Gate) -> None:
def get_circuit_matrix(circuit: Circuit) -> NDArray[np.complex128]:
"""Compute the (large) unitary matrix corresponding to the circuit.

This matrix has 4**n elements, where n is the number of qubits. Result is stored as a numpy array of complex
numbers.
This matrix has $4^n$ elements, where $n$ is the number of qubits.

Args:
circuit (Circuit): The circuit for which to compute the matrix.

Returns:
Matrix representation of the circuit.

"""
impl = _CircuitMatrixCalculator(circuit.qubit_register_size)

Expand Down
31 changes: 20 additions & 11 deletions opensquirrel/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@


def normalize_angle(x: SupportsFloat) -> float:
r"""Normalize the angle to be in between the range of $(-\pi, \pi]$.
"""Normalize the angle to be in between the range of $(-\\pi, \\pi]$.

Args:
x: value to normalize.
x (SupportsFloat): value to normalize.

Returns:
The normalized angle.

"""
x = float(x)
t = x - tau * (x // tau + 1)
Expand All @@ -38,11 +39,12 @@ def are_matrices_equivalent_up_to_global_phase(
"""Checks whether two matrices are equivalent up to a global phase.

Args:
matrix_a: first matrix.
matrix_b: second matrix.
matrix_a (NDArray[np.complex128]): first matrix.
matrix_b (NDArray[np.complex128]): second matrix.

Returns:
Whether two matrices are equivalent up to a global phase.
True if two matrices are equivalent up to a global phase, otherwise False.

"""
first_non_zero = next(
(i, j) for i in range(matrix_a.shape[0]) for j in range(matrix_a.shape[1]) if abs(matrix_a[i, j]) > ATOL
Expand All @@ -60,21 +62,28 @@ def is_identity_matrix_up_to_a_global_phase(matrix: NDArray[np.complex128]) -> b
"""Checks whether matrix is an identity matrix up to a global phase.

Args:
matrix: matrix to check.
matrix (NDArray[np.complex128]): matrix to check.

Returns:
Whether matrix is an identity matrix up to a global phase.
True if matrix is an identity matrix up to a global phase, otherwise False.

"""
return are_matrices_equivalent_up_to_global_phase(matrix, np.eye(matrix.shape[0], dtype=np.complex128))


def repr_round(value: float | BaseAxis | NDArray[np.complex128], decimals: int = REPR_DECIMALS) -> str:
"""
Given a numerical value (of type `float`, `Axis`, or `NDArray[np.complex128]`):
"""Given a numerical value:

- rounds it to `REPR_DECIMALS`,
- converts it to string, and
- removes the newlines.
- removes any newlines.

Args:
value (float | BaseAxis | NDArray[np.complex128]): The numerical value to represent.
decimals (int): Number of decimals to round to. Default is `REPR_DECIMALS`.

Returns:
A single-line string representation of a numerical value.
A single-line string representation of a rounded numerical value.

"""
return f"{np.round(value, decimals)}".replace("\n", "")
9 changes: 9 additions & 0 deletions opensquirrel/default_instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,13 @@


def is_anonymous_gate(name: str) -> bool:
"""Checks whether the input name corresponds to a non-default gate.

Args:
name (str): Name of the gate.

Returns:
True if the name does not correspond to a default gate, otherwise False.

"""
return name not in default_gate_set
Loading