• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Qiskit / qiskit / 13659671616

04 Mar 2025 05:42PM CUT coverage: 87.12% (+0.06%) from 87.064%
13659671616

Pull #13836

github

web-flow
Merge 4073279af into 091228cf1
Pull Request #13836: `SparseObservable` evolution

246 of 250 new or added lines in 6 files covered. (98.4%)

187 existing lines in 13 files now uncovered.

76554 of 87872 relevant lines covered (87.12%)

326790.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.16
/crates/circuit/src/circuit_instruction.rs
1
// This code is part of Qiskit.
2
//
3
// (C) Copyright IBM 2023
4
//
5
// This code is licensed under the Apache License, Version 2.0. You may
6
// obtain a copy of this license in the LICENSE.txt file in the root directory
7
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8
//
9
// Any modifications or derivative works of this code must retain this
10
// copyright notice, and modified files need to carry a notice indicating
11
// that they have been altered from the originals.
12

13
#[cfg(feature = "cache_pygates")]
14
use std::sync::OnceLock;
15

16
use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2};
17
use pyo3::basic::CompareOp;
18
use pyo3::exceptions::{PyDeprecationWarning, PyTypeError};
19
use pyo3::prelude::*;
20

21
use pyo3::types::{PyBool, PyList, PyTuple, PyType};
22
use pyo3::IntoPyObjectExt;
23
use pyo3::{intern, PyObject, PyResult};
24

25
use nalgebra::{MatrixView2, MatrixView4};
26
use num_complex::Complex64;
27
use smallvec::SmallVec;
28

29
use crate::imports::{
30
    CONTROLLED_GATE, CONTROL_FLOW_OP, GATE, INSTRUCTION, OPERATION, WARNINGS_WARN,
31
};
32
use crate::operations::{
33
    ArrayType, Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate,
34
    StandardInstruction, StandardInstructionType, UnitaryGate,
35
};
36
use crate::packed_instruction::PackedOperation;
37

38
/// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and
39
/// various operands.
40
///
41
/// .. note::
42
///
43
///     There is some possible confusion in the names of this class, :class:`~.circuit.Instruction`,
44
///     and :class:`~.circuit.Operation`, and this class's attribute :attr:`operation`.  Our
45
///     preferred terminology is by analogy to assembly languages, where an "instruction" is made up
46
///     of an "operation" and its "operands".
47
///
48
///     Historically, :class:`~.circuit.Instruction` came first, and originally contained the qubits
49
///     it operated on and any parameters, so it was a true "instruction".  Over time,
50
///     :class:`.QuantumCircuit` became responsible for tracking qubits and clbits, and the class
51
///     became better described as an "operation".  Changing the name of such a core object would be
52
///     a very unpleasant API break for users, and so we have stuck with it.
53
///
54
///     This class was created to provide a formal "instruction" context object in
55
///     :class:`.QuantumCircuit.data`, which had long been made of ad-hoc tuples.  With this, and
56
///     the advent of the :class:`~.circuit.Operation` interface for adding more complex objects to
57
///     circuits, we took the opportunity to correct the historical naming.  For the time being,
58
///     this leads to an awkward case where :attr:`.CircuitInstruction.operation` is often an
59
///     :class:`~.circuit.Instruction` instance (:class:`~.circuit.Instruction` implements the
60
///     :class:`.Operation` interface), but as the :class:`.Operation` interface gains more use,
61
///     this confusion will hopefully abate.
62
///
63
/// .. warning::
64
///
65
///     This is a lightweight internal class and there is minimal error checking; you must respect
66
///     the type hints when using it.  It is the user's responsibility to ensure that direct
67
///     mutations of the object do not invalidate the types, nor the restrictions placed on it by
68
///     its context.  Typically this will mean, for example, that :attr:`qubits` must be a sequence
69
///     of distinct items, with no duplicates.
70
#[pyclass(freelist = 20, sequence, module = "qiskit._accelerate.circuit")]
11,211,134✔
71
#[derive(Clone, Debug)]
72
pub struct CircuitInstruction {
73
    pub operation: PackedOperation,
74
    /// A sequence of the qubits that the operation is applied to.
75
    #[pyo3(get)]
76
    pub qubits: Py<PyTuple>,
24✔
77
    /// A sequence of the classical bits that this operation reads from or writes to.
78
    #[pyo3(get)]
79
    pub clbits: Py<PyTuple>,
24✔
80
    pub params: SmallVec<[Param; 3]>,
81
    pub label: Option<Box<String>>,
82
    #[cfg(feature = "cache_pygates")]
83
    pub py_op: OnceLock<Py<PyAny>>,
84
}
85

86
impl CircuitInstruction {
87
    /// Get the Python-space operation, ensuring that it is mutable from Python space (singleton
88
    /// gates might not necessarily satisfy this otherwise).
89
    ///
90
    /// This returns the cached instruction if valid, but does not replace the cache if it created a
91
    /// new mutable object; the expectation is that any mutations to the Python object need
92
    /// assigning back to the `CircuitInstruction` completely to ensure data coherence between Rust
93
    /// and Python spaces.  We can't protect entirely against that, but we can make it a bit harder
94
    /// for standard-gate getters to accidentally do the wrong thing.
95
    pub fn get_operation_mut<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
×
96
        let out = self.get_operation(py)?.into_bound(py);
×
97
        if out.getattr(intern!(py, "mutable"))?.is_truthy()? {
×
98
            Ok(out)
×
99
        } else {
100
            out.call_method0(intern!(py, "to_mutable"))
×
101
        }
102
    }
×
103
}
104

105
#[pymethods]
1,809,988✔
106
impl CircuitInstruction {
107
    #[new]
108
    #[pyo3(signature = (operation, qubits=None, clbits=None))]
109
    pub fn py_new(
605,674✔
110
        operation: &Bound<PyAny>,
605,674✔
111
        qubits: Option<Bound<PyAny>>,
605,674✔
112
        clbits: Option<Bound<PyAny>>,
605,674✔
113
    ) -> PyResult<Self> {
605,674✔
114
        let py = operation.py();
605,674✔
115
        let op_parts = operation.extract::<OperationFromPython>()?;
605,674✔
116

117
        Ok(Self {
118
            operation: op_parts.operation,
605,674✔
119
            qubits: as_tuple(py, qubits)?.unbind(),
605,674✔
120
            clbits: as_tuple(py, clbits)?.unbind(),
605,674✔
121
            params: op_parts.params,
605,674✔
122
            label: op_parts.label,
605,674✔
123
            #[cfg(feature = "cache_pygates")]
605,674✔
124
            py_op: operation.clone().unbind().into(),
605,674✔
125
        })
126
    }
605,674✔
127

128
    #[pyo3(signature = (standard, qubits, params, label=None))]
129
    #[staticmethod]
130
    pub fn from_standard(
641,884✔
131
        py: Python,
641,884✔
132
        standard: StandardGate,
641,884✔
133
        qubits: Option<Bound<PyAny>>,
641,884✔
134
        params: SmallVec<[Param; 3]>,
641,884✔
135
        label: Option<String>,
641,884✔
136
    ) -> PyResult<Self> {
641,884✔
137
        Ok(Self {
641,884✔
138
            operation: standard.into(),
641,884✔
139
            qubits: as_tuple(py, qubits)?.unbind(),
641,884✔
140
            clbits: PyTuple::empty(py).unbind(),
641,884✔
141
            params,
641,884✔
142
            label: label.map(Box::new),
641,884✔
143
            #[cfg(feature = "cache_pygates")]
641,884✔
144
            py_op: OnceLock::new(),
641,884✔
145
        })
146
    }
641,884✔
147

148
    /// Returns a shallow copy.
149
    ///
150
    /// Returns:
151
    ///     CircuitInstruction: The shallow copy.
152
    pub fn copy(&self) -> Self {
×
153
        self.clone()
×
154
    }
×
155

156
    /// The logical operation that this instruction represents an execution of.
157
    #[getter]
158
    pub fn get_operation(&self, py: Python) -> PyResult<PyObject> {
8,304,986✔
159
        // This doesn't use `get_or_init` because a) the initialiser is fallible and
160
        // `get_or_try_init` isn't stable, and b) the initialiser can yield to the Python
161
        // interpreter, which might suspend the thread and allow another to inadvertantly attempt to
162
        // re-enter the cache setter, which isn't safe.
163

164
        #[cfg(feature = "cache_pygates")]
165
        {
166
            if let Some(cached_op) = self.py_op.get() {
8,304,986✔
167
                return Ok(cached_op.clone_ref(py));
5,083,772✔
168
            }
3,221,214✔
169
        }
170

171
        let out = match self.operation.view() {
3,221,214✔
172
            OperationRef::StandardGate(standard) => standard
2,880,846✔
173
                .create_py_op(
2,880,846✔
174
                    py,
2,880,846✔
175
                    Some(&self.params),
2,880,846✔
176
                    self.label.as_ref().map(|x| x.as_str()),
2,880,846✔
177
                )?
2,880,846✔
178
                .into_any(),
2,880,846✔
179
            OperationRef::StandardInstruction(instruction) => instruction
45,564✔
180
                .create_py_op(
45,564✔
181
                    py,
45,564✔
182
                    Some(&self.params),
45,564✔
183
                    self.label.as_ref().map(|x| x.as_str()),
45,564✔
184
                )?
45,564✔
185
                .into_any(),
45,564✔
186
            OperationRef::Gate(gate) => gate.gate.clone_ref(py),
181,398✔
187
            OperationRef::Instruction(instruction) => instruction.instruction.clone_ref(py),
27,138✔
188
            OperationRef::Operation(operation) => operation.operation.clone_ref(py),
1,518✔
189
            OperationRef::Unitary(unitary) => unitary
84,750✔
190
                .create_py_op(py, self.label.as_ref().map(|x| x.as_str()))?
84,750✔
191
                .into_any(),
84,750✔
192
        };
193

194
        #[cfg(feature = "cache_pygates")]
195
        {
3,221,214✔
196
            self.py_op.get_or_init(|| out.clone_ref(py));
3,221,214✔
197
        }
3,221,214✔
198

3,221,214✔
199
        Ok(out)
3,221,214✔
200
    }
8,304,986✔
201

202
    /// Returns the Instruction name corresponding to the op for this node
203
    #[getter]
204
    fn get_name(&self) -> &str {
673,488✔
205
        self.operation.name()
673,488✔
206
    }
673,488✔
207

208
    #[getter]
209
    fn get_params(&self) -> &[Param] {
69,600✔
210
        self.params.as_slice()
69,600✔
211
    }
69,600✔
212

213
    #[getter]
214
    fn matrix<'py>(&'py self, py: Python<'py>) -> Option<Bound<'py, PyArray2<Complex64>>> {
×
215
        let matrix = self.operation.view().matrix(&self.params);
×
216
        matrix.map(move |mat| mat.into_pyarray(py))
×
217
    }
×
218

219
    #[getter]
220
    fn label(&self) -> Option<&str> {
2✔
221
        self.label.as_ref().map(|label| label.as_str())
2✔
222
    }
2✔
223

224
    /// Is the :class:`.Operation` contained in this instruction a Qiskit standard gate?
225
    pub fn is_standard_gate(&self) -> bool {
102✔
226
        self.operation.try_standard_gate().is_some()
102✔
227
    }
102✔
228

229
    /// Is the :class:`.Operation` contained in this instruction a subclass of
230
    /// :class:`.ControlledGate`?
UNCOV
231
    pub fn is_controlled_gate(&self, py: Python) -> PyResult<bool> {
×
UNCOV
232
        match self.operation.view() {
×
UNCOV
233
            OperationRef::StandardGate(standard) => Ok(standard.num_ctrl_qubits() != 0),
×
234
            OperationRef::Gate(gate) => gate
×
235
                .gate
×
236
                .bind(py)
×
237
                .is_instance(CONTROLLED_GATE.get_bound(py)),
×
238
            _ => Ok(false),
×
239
        }
UNCOV
240
    }
×
241

242
    /// Is the :class:`.Operation` contained in this node a directive?
243
    pub fn is_directive(&self) -> bool {
47,092✔
244
        self.operation.directive()
47,092✔
245
    }
47,092✔
246

247
    /// Is the :class:`.Operation` contained in this instruction a control-flow operation (i.e. an
248
    /// instance of :class:`.ControlFlowOp`)?
249
    pub fn is_control_flow(&self) -> bool {
381,572✔
250
        self.operation.control_flow()
381,572✔
251
    }
381,572✔
252

253
    /// Does this instruction contain any :class:`.ParameterExpression` parameters?
254
    pub fn is_parameterized(&self) -> bool {
88✔
255
        self.params
88✔
256
            .iter()
88✔
257
            .any(|x| matches!(x, Param::ParameterExpression(_)))
172✔
258
    }
88✔
259

260
    /// Creates a shallow copy with the given fields replaced.
261
    ///
262
    /// Returns:
263
    ///     CircuitInstruction: A new instance with the given fields replaced.
264
    #[pyo3(signature=(operation=None, qubits=None, clbits=None, params=None))]
265
    pub fn replace(
932,696✔
266
        &self,
932,696✔
267
        py: Python,
932,696✔
268
        operation: Option<&Bound<PyAny>>,
932,696✔
269
        qubits: Option<Bound<PyAny>>,
932,696✔
270
        clbits: Option<Bound<PyAny>>,
932,696✔
271
        params: Option<Bound<PyAny>>,
932,696✔
272
    ) -> PyResult<Self> {
932,696✔
273
        let qubits = match qubits {
932,696✔
274
            None => self.qubits.clone_ref(py),
208,154✔
275
            Some(qubits) => as_tuple(py, Some(qubits))?.unbind(),
724,542✔
276
        };
277
        let clbits = match clbits {
932,696✔
278
            None => self.clbits.clone_ref(py),
459,440✔
279
            Some(clbits) => as_tuple(py, Some(clbits))?.unbind(),
473,256✔
280
        };
281
        let params = params
932,696✔
282
            .map(|params| params.extract::<SmallVec<[Param; 3]>>())
932,696✔
283
            .transpose()?;
932,696✔
284

285
        if let Some(operation) = operation {
932,696✔
286
            let op_parts = operation.extract::<OperationFromPython>()?;
208,250✔
287
            Ok(Self {
208,250✔
288
                operation: op_parts.operation,
208,250✔
289
                qubits,
208,250✔
290
                clbits,
208,250✔
291
                params: params.unwrap_or(op_parts.params),
208,250✔
292
                label: op_parts.label,
208,250✔
293
                #[cfg(feature = "cache_pygates")]
208,250✔
294
                py_op: operation.clone().unbind().into(),
208,250✔
295
            })
208,250✔
296
        } else {
297
            Ok(Self {
724,446✔
298
                operation: self.operation.clone(),
724,446✔
299
                qubits,
724,446✔
300
                clbits,
724,446✔
301
                params: params.unwrap_or_else(|| self.params.clone()),
724,446✔
302
                label: self.label.clone(),
724,446✔
303
                #[cfg(feature = "cache_pygates")]
724,446✔
304
                py_op: self.py_op.clone(),
724,446✔
305
            })
724,446✔
306
        }
307
    }
932,696✔
308

309
    pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult<PyObject> {
48,110✔
310
        (
48,110✔
311
            self.get_operation(py)?,
48,110✔
312
            self.qubits.bind(py),
48,110✔
313
            self.clbits.bind(py),
48,110✔
314
        )
48,110✔
315
            .into_py_any(py)
48,110✔
316
    }
48,110✔
317

318
    pub fn __repr__(self_: &Bound<Self>, py: Python<'_>) -> PyResult<String> {
32✔
319
        let type_name = self_.get_type().qualname()?;
32✔
320
        let r = self_.try_borrow()?;
32✔
321
        Ok(format!(
32✔
322
            "{}(operation={}, qubits={}, clbits={})",
32✔
323
            type_name,
32✔
324
            r.get_operation(py)?.bind(py).repr()?,
32✔
325
            r.qubits.bind(py).repr()?,
32✔
326
            r.clbits.bind(py).repr()?
32✔
327
        ))
328
    }
32✔
329

330
    // Legacy tuple-like interface support.
331
    //
332
    // For a best attempt at API compatibility during the transition to using this new class, we need
333
    // the interface to behave exactly like the old 3-tuple `(inst, qargs, cargs)` if it's treated
334
    // like that via unpacking or similar.  That means that the `parameters` field is completely
335
    // absent, and the qubits and clbits must be converted to lists.
336
    pub fn _legacy_format<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
12✔
337
        PyTuple::new(
12✔
338
            py,
12✔
339
            [
12✔
340
                self.get_operation(py)?,
12✔
341
                self.qubits.bind(py).to_list().into(),
12✔
342
                self.clbits.bind(py).to_list().into(),
12✔
343
            ],
344
        )
345
    }
12✔
346

347
    pub fn __getitem__(&self, py: Python<'_>, key: &Bound<PyAny>) -> PyResult<PyObject> {
×
348
        warn_on_legacy_circuit_instruction_iteration(py)?;
×
349
        self._legacy_format(py)?
×
350
            .as_any()
×
351
            .get_item(key)?
×
352
            .into_py_any(py)
×
353
    }
×
354

355
    pub fn __iter__(&self, py: Python<'_>) -> PyResult<PyObject> {
12✔
356
        warn_on_legacy_circuit_instruction_iteration(py)?;
12✔
357
        self._legacy_format(py)?
12✔
358
            .as_any()
12✔
359
            .try_iter()?
12✔
360
            .into_py_any(py)
12✔
361
    }
12✔
362

363
    pub fn __len__(&self, py: Python) -> PyResult<usize> {
12✔
364
        warn_on_legacy_circuit_instruction_iteration(py)?;
12✔
365
        Ok(3)
12✔
366
    }
12✔
367

368
    pub fn __richcmp__(
450✔
369
        self_: &Bound<Self>,
450✔
370
        other: &Bound<PyAny>,
450✔
371
        op: CompareOp,
450✔
372
        py: Python<'_>,
450✔
373
    ) -> PyResult<PyObject> {
450✔
374
        fn params_eq(py: Python, left: &[Param], right: &[Param]) -> PyResult<bool> {
372✔
375
            if left.len() != right.len() {
372✔
376
                return Ok(false);
×
377
            }
372✔
378
            for (left, right) in left.iter().zip(right) {
372✔
379
                let eq = match left {
18✔
380
                    Param::Float(left) => match right {
12✔
381
                        Param::Float(right) => left == right,
12✔
382
                        Param::ParameterExpression(right) | Param::Obj(right) => {
×
383
                            right.bind(py).eq(left)?
×
384
                        }
385
                    },
386
                    Param::ParameterExpression(left) | Param::Obj(left) => match right {
6✔
387
                        Param::Float(right) => left.bind(py).eq(right)?,
×
388
                        Param::ParameterExpression(right) | Param::Obj(right) => {
6✔
389
                            left.bind(py).eq(right)?
6✔
390
                        }
391
                    },
392
                };
393
                if !eq {
18✔
394
                    return Ok(false);
2✔
395
                }
16✔
396
            }
397
            Ok(true)
370✔
398
        }
372✔
399

400
        fn eq(
450✔
401
            py: Python<'_>,
450✔
402
            self_: &Bound<CircuitInstruction>,
450✔
403
            other: &Bound<PyAny>,
450✔
404
        ) -> PyResult<Option<bool>> {
450✔
405
            if self_.is(other) {
450✔
406
                return Ok(Some(true));
×
407
            }
450✔
408

409
            let self_ = self_.try_borrow()?;
450✔
410

411
            if other.is_instance_of::<PyTuple>() {
450✔
412
                return Ok(Some(self_._legacy_format(py)?.eq(other)?));
×
413
            }
450✔
414
            let Ok(other) = other.downcast::<CircuitInstruction>() else {
450✔
415
                return Ok(None);
×
416
            };
417
            let other = other.try_borrow()?;
450✔
418

419
            Ok(Some(
420
                self_.qubits.bind(py).eq(other.qubits.bind(py))?
450✔
421
                    && self_.clbits.bind(py).eq(other.clbits.bind(py))?
434✔
422
                    && self_.operation.py_eq(py, &other.operation)?
434✔
423
                    && (self_.operation.try_standard_gate().is_none()
430✔
424
                        || params_eq(py, &self_.params, &other.params)?),
372✔
425
            ))
426
        }
450✔
427

428
        Ok(match op {
450✔
429
            CompareOp::Eq => match eq(py, self_, other)? {
448✔
430
                Some(res) => PyBool::new(py, res).to_owned().into_any().unbind(),
448✔
431
                None => py.NotImplemented(),
×
432
            },
433
            CompareOp::Ne => match eq(py, self_, other)? {
2✔
434
                Some(res) => PyBool::new(py, !res).to_owned().into_any().unbind(),
2✔
435
                None => py.NotImplemented(),
×
436
            },
437
            _ => py.NotImplemented(),
×
438
        })
439
    }
450✔
440
}
441

442
/// A container struct that contains the conversion from some `Operation` subclass input, on its way
443
/// to becoming a `PackedInstruction`.
444
///
445
/// This is the primary way of converting an incoming `Gate` / `Instruction` / `Operation` from
446
/// Python space into Rust-space data.  A typical access pattern is:
447
///
448
/// ```rust
449
/// #[pyfunction]
450
/// fn accepts_op_from_python(ob: &Bound<PyAny>) -> PyResult<()> {
451
///     let py_op = ob.extract::<OperationFromPython>()?;
452
///     // ... use `py_op.operation`, `py_op.params`, etc.
453
///     Ok(())
454
/// }
455
/// ```
456
///
457
/// though you can also accept `ob: OperationFromPython` directly, if you don't also need a handle
458
/// to the Python object that it came from.  The handle is useful for the Python-operation caching.
459
#[derive(Debug)]
460
pub struct OperationFromPython {
461
    pub operation: PackedOperation,
462
    pub params: SmallVec<[Param; 3]>,
463
    pub label: Option<Box<String>>,
464
}
465

466
impl<'py> FromPyObject<'py> for OperationFromPython {
467
    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1,422,286✔
468
        let py = ob.py();
1,422,286✔
469
        let ob_type = ob
1,422,286✔
470
            .getattr(intern!(py, "base_class"))
1,422,286✔
471
            .ok()
1,422,286✔
472
            .map(|base| base.downcast_into::<PyType>())
1,422,286✔
473
            .transpose()?
1,422,286✔
474
            .unwrap_or_else(|| ob.get_type());
1,397,714✔
475

1,397,714✔
476
        let extract_params = || {
1,397,714✔
477
            ob.getattr(intern!(py, "params"))
1,391,304✔
478
                .ok()
1,391,304✔
479
                .map(|params| params.extract())
1,391,304✔
480
                .transpose()
1,391,304✔
481
                .map(|params| params.unwrap_or_default())
1,391,304✔
482
        };
1,391,304✔
483

484
        let extract_params_no_coerce = || {
1,397,714✔
485
            ob.getattr(intern!(py, "params"))
4,602✔
486
                .ok()
4,602✔
487
                .map(|params| {
4,602✔
488
                    params
4,602✔
489
                        .try_iter()?
4,602✔
490
                        .map(|p| Param::extract_no_coerce(&p?))
4,602✔
491
                        .collect()
4,602✔
492
                })
4,602✔
493
                .transpose()
4,602✔
494
                .map(|params| params.unwrap_or_default())
4,602✔
495
        };
4,602✔
496

497
        let extract_label = || -> PyResult<Option<Box<String>>> {
1,397,714✔
498
            let raw = ob.getattr(intern!(py, "label"))?;
1,394,552✔
499
            Ok(raw.extract::<Option<String>>()?.map(Box::new))
1,394,552✔
500
        };
1,394,552✔
501

502
        'standard_gate: {
503
            // Our Python standard gates have a `_standard_gate` field at the class level so we can
504
            // quickly identify them here without an `isinstance` check.
505
            let Some(standard) = ob_type
1,397,714✔
506
                .getattr(intern!(py, "_standard_gate"))
1,397,714✔
507
                .and_then(|standard| standard.extract::<StandardGate>())
1,397,714✔
508
                .ok()
1,397,714✔
509
            else {
510
                break 'standard_gate;
593,302✔
511
            };
512

513
            // If the instruction is a controlled gate with a not-all-ones control state, it doesn't
514
            // fit our definition of standard.  We abuse the fact that we know our standard-gate
515
            // mapping to avoid an `isinstance` check on `ControlledGate` - a standard gate has
516
            // nonzero `num_ctrl_qubits` iff it is a `ControlledGate`.
517
            //
518
            // `ControlledGate` also has a `base_gate` attribute related to its historical
519
            // implementation, which technically allows mutations from Python space.  The only
520
            // mutation of a standard gate's `base_gate` that wouldn't have already broken the
521
            // Python-space data model is setting a label, so we just catch that case and default
522
            // back to non-standard-gate handling in that case.
523
            if standard.num_ctrl_qubits() != 0
804,412✔
524
                && ((ob.getattr(intern!(py, "ctrl_state"))?.extract::<usize>()?
301,902✔
525
                    != (1 << standard.num_ctrl_qubits()) - 1)
301,902✔
526
                    || !ob
301,776✔
527
                        .getattr(intern!(py, "base_gate"))?
301,776✔
528
                        .getattr(intern!(py, "label"))?
301,776✔
529
                        .is_none())
301,776✔
530
            {
531
                break 'standard_gate;
132✔
532
            }
804,280✔
533
            return Ok(OperationFromPython {
804,280✔
534
                operation: PackedOperation::from_standard_gate(standard),
804,280✔
535
                params: extract_params()?,
804,280✔
536
                label: extract_label()?,
804,280✔
537
            });
538
        }
539
        'standard_instr: {
540
            // Our Python standard instructions have a `_standard_instruction_type` field at the
541
            // class level so we can quickly identify them here without an `isinstance` check.
542
            // Once we know the type, we query the object for any type-specific fields we need to
543
            // read (e.g. a Barrier's number of qubits) to build the Rust representation.
544
            let Some(standard_type) = ob_type
593,434✔
545
                .getattr(intern!(py, "_standard_instruction_type"))
593,434✔
546
                .and_then(|standard| standard.extract::<StandardInstructionType>())
593,434✔
547
                .ok()
593,434✔
548
            else {
549
                break 'standard_instr;
493,198✔
550
            };
551
            let standard = match standard_type {
100,236✔
552
                StandardInstructionType::Barrier => {
553
                    let num_qubits = ob.getattr(intern!(py, "num_qubits"))?.extract()?;
11,002✔
554
                    StandardInstruction::Barrier(num_qubits)
11,002✔
555
                }
556
                StandardInstructionType::Delay => {
557
                    let unit = ob.getattr(intern!(py, "unit"))?.extract()?;
4,602✔
558
                    return Ok(OperationFromPython {
559
                        operation: PackedOperation::from_standard_instruction(
4,602✔
560
                            StandardInstruction::Delay(unit),
4,602✔
561
                        ),
4,602✔
562
                        // If the delay's duration is a Python int, we preserve it rather than
4,602✔
563
                        // coercing it to a float (e.g. when unit is 'dt').
4,602✔
564
                        params: extract_params_no_coerce()?,
4,602✔
565
                        label: extract_label()?,
4,602✔
566
                    });
567
                }
568
                StandardInstructionType::Measure => StandardInstruction::Measure,
22,200✔
569
                StandardInstructionType::Reset => StandardInstruction::Reset,
62,432✔
570
            };
571
            return Ok(OperationFromPython {
572
                operation: PackedOperation::from_standard_instruction(standard),
95,634✔
573
                params: extract_params()?,
95,634✔
574
                label: extract_label()?,
95,634✔
575
            });
576
        }
577

578
        // We need to check by name here to avoid a circular import during initial loading
579
        if ob.getattr(intern!(py, "name"))?.extract::<String>()? == "unitary" {
493,198✔
580
            let params = extract_params()?;
255,140✔
581
            if let Param::Obj(data) = &params[0] {
255,140✔
582
                let py_matrix: PyReadonlyArray2<Complex64> = data.extract(py)?;
255,140✔
583
                let matrix: Option<MatrixView2<Complex64>> = py_matrix.try_as_matrix();
255,140✔
584
                if let Some(x) = matrix {
255,140✔
585
                    let unitary_gate = Box::new(UnitaryGate {
81,268✔
586
                        array: ArrayType::OneQ(x.into_owned()),
81,268✔
587
                    });
81,268✔
588
                    return Ok(OperationFromPython {
81,268✔
589
                        operation: PackedOperation::from_unitary(unitary_gate),
81,268✔
590
                        params: SmallVec::new(),
81,268✔
591
                        label: extract_label()?,
81,268✔
592
                    });
593
                }
173,872✔
594
                let matrix: Option<MatrixView4<Complex64>> = py_matrix.try_as_matrix();
173,872✔
595
                if let Some(x) = matrix {
173,872✔
596
                    let unitary_gate = Box::new(UnitaryGate {
142✔
597
                        array: ArrayType::TwoQ(x.into_owned()),
142✔
598
                    });
142✔
599
                    return Ok(OperationFromPython {
142✔
600
                        operation: PackedOperation::from_unitary(unitary_gate),
142✔
601
                        params: SmallVec::new(),
142✔
602
                        label: extract_label()?,
142✔
603
                    });
604
                } else {
605
                    let unitary_gate = Box::new(UnitaryGate {
173,730✔
606
                        array: ArrayType::NDArray(py_matrix.as_array().to_owned()),
173,730✔
607
                    });
173,730✔
608
                    return Ok(OperationFromPython {
173,730✔
609
                        operation: PackedOperation::from_unitary(unitary_gate),
173,730✔
610
                        params: SmallVec::new(),
173,730✔
611
                        label: extract_label()?,
173,730✔
612
                    });
613
                };
614
            }
×
615
        }
236,250✔
616

617
        if ob_type.is_subclass(GATE.get_bound(py))? {
236,250✔
618
            let params = extract_params()?;
149,406✔
619
            let gate = Box::new(PyGate {
149,406✔
620
                qubits: ob.getattr(intern!(py, "num_qubits"))?.extract()?,
149,406✔
621
                clbits: 0,
622
                params: params.len() as u32,
149,406✔
623
                op_name: ob.getattr(intern!(py, "name"))?.extract()?,
149,406✔
624
                gate: ob.clone().unbind(),
149,406✔
625
            });
149,406✔
626
            return Ok(OperationFromPython {
149,406✔
627
                operation: PackedOperation::from_gate(gate),
149,406✔
628
                params,
149,406✔
629
                label: extract_label()?,
149,406✔
630
            });
631
        }
86,844✔
632
        if ob_type.is_subclass(INSTRUCTION.get_bound(py))? {
86,844✔
633
            let params = extract_params()?;
85,490✔
634
            let instruction = Box::new(PyInstruction {
85,490✔
635
                qubits: ob.getattr(intern!(py, "num_qubits"))?.extract()?,
85,490✔
636
                clbits: ob.getattr(intern!(py, "num_clbits"))?.extract()?,
85,490✔
637
                params: params.len() as u32,
85,490✔
638
                op_name: ob.getattr(intern!(py, "name"))?.extract()?,
85,490✔
639
                control_flow: ob.is_instance(CONTROL_FLOW_OP.get_bound(py))?,
85,490✔
640
                instruction: ob.clone().unbind(),
85,490✔
641
            });
85,490✔
642
            return Ok(OperationFromPython {
85,490✔
643
                operation: PackedOperation::from_instruction(instruction),
85,490✔
644
                params,
85,490✔
645
                label: extract_label()?,
85,490✔
646
            });
647
        }
1,354✔
648
        if ob_type.is_subclass(OPERATION.get_bound(py))? {
1,354✔
649
            let params = extract_params()?;
1,354✔
650
            let operation = Box::new(PyOperation {
1,354✔
651
                qubits: ob.getattr(intern!(py, "num_qubits"))?.extract()?,
1,354✔
652
                clbits: ob.getattr(intern!(py, "num_clbits"))?.extract()?,
1,354✔
653
                params: params.len() as u32,
1,354✔
654
                op_name: ob.getattr(intern!(py, "name"))?.extract()?,
1,354✔
655
                operation: ob.clone().unbind(),
1,354✔
656
            });
1,354✔
657
            return Ok(OperationFromPython {
1,354✔
658
                operation: PackedOperation::from_operation(operation),
1,354✔
659
                params,
1,354✔
660
                label: None,
1,354✔
661
            });
1,354✔
662
        }
×
663
        Err(PyTypeError::new_err(format!("invalid input: {}", ob)))
×
664
    }
1,422,286✔
665
}
666

667
/// Convert a sequence-like Python object to a tuple.
668
fn as_tuple<'py>(py: Python<'py>, seq: Option<Bound<'py, PyAny>>) -> PyResult<Bound<'py, PyTuple>> {
3,051,030✔
669
    let Some(seq) = seq else {
3,051,030✔
670
        return Ok(PyTuple::empty(py));
34,156✔
671
    };
672
    if seq.is_instance_of::<PyTuple>() {
3,016,874✔
673
        Ok(seq.downcast_into_exact::<PyTuple>()?)
1,275,838✔
674
    } else if seq.is_instance_of::<PyList>() {
1,741,036✔
675
        Ok(seq.downcast_exact::<PyList>()?.to_tuple())
1,718,562✔
676
    } else {
677
        // New tuple from iterable.
678
        PyTuple::new(
679
            py,
22,474✔
680
            seq.try_iter()?
22,474✔
681
                .map(|o| Ok(o?.unbind()))
55,008✔
682
                .collect::<PyResult<Vec<PyObject>>>()?,
22,474✔
683
        )
684
    }
685
}
3,051,030✔
686

687
/// Issue a Python `DeprecationWarning` about using the legacy tuple-like interface to
688
/// `CircuitInstruction`.
689
///
690
/// Beware the `stacklevel` here doesn't work quite the same way as it does in Python as Rust-space
691
/// calls are completely transparent to Python.
692
#[inline]
693
fn warn_on_legacy_circuit_instruction_iteration(py: Python) -> PyResult<()> {
24✔
694
    WARNINGS_WARN
24✔
695
        .get_bound(py)
24✔
696
        .call1((
24✔
697
            intern!(
24✔
698
                py,
24✔
699
                concat!(
24✔
700
                    "Treating CircuitInstruction as an iterable is deprecated legacy behavior",
24✔
701
                    " since Qiskit 1.2, and will be removed in Qiskit 2.0.",
24✔
702
                    " Instead, use the `operation`, `qubits` and `clbits` named attributes."
24✔
703
                )
24✔
704
            ),
24✔
705
            py.get_type::<PyDeprecationWarning>(),
24✔
706
            // Stack level.  Compared to Python-space calls to `warn`, this is unusually low
24✔
707
            // beacuse all our internal call structure is now Rust-space and invisible to Python.
24✔
708
            1,
24✔
709
        ))
24✔
710
        .map(|_| ())
24✔
711
}
24✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc