Skip to content

Commit

Permalink
Fix: Move calibration back to rust InstructionProperties
Browse files Browse the repository at this point in the history
- Accessing `Target` through rust was disabling access to the calibration attribute.
- Return Python keys object when calling `Target.keys()`
  • Loading branch information
raynelfss committed Jul 31, 2024
1 parent 27cf414 commit 9d627f7
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 89 deletions.
114 changes: 91 additions & 23 deletions crates/accelerate/src/target_transpiler/instruction_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use pyo3::{prelude::*, pyclass};
use pyo3::{prelude::*, pyclass, types::IntoPyDict};
use qiskit_circuit::imports::ImportOnceCell;

static SCHEDULE: ImportOnceCell = ImportOnceCell::new("qiskit.pulse.schedule", "Schedule");
static SCHEDULE_BLOCK: ImportOnceCell =
ImportOnceCell::new("qiskit.pulse.schedule", "ScheduleBlock");
static SCHEDULE_DEF: ImportOnceCell =
ImportOnceCell::new("qiskit.pulse.calibration_entries", "ScheduleDef");

/**
A representation of an ``InstructionProperties`` object.
Expand All @@ -26,6 +33,8 @@ pub struct InstructionProperties {
pub duration: Option<f64>,
#[pyo3(get, set)]
pub error: Option<f64>,
#[pyo3(get)]
_calibration: Option<PyObject>,
}

#[pymethods]
Expand All @@ -39,34 +48,93 @@ impl InstructionProperties {
/// set of qubits.
/// calibration (Option<PyObject>): The pulse representation of the instruction.
#[new]
#[pyo3(signature = (duration=None, error=None))]
pub fn new(_py: Python<'_>, duration: Option<f64>, error: Option<f64>) -> Self {
Self { error, duration }
#[pyo3(signature = (duration=None, error=None, calibration=None))]
pub fn new(
py: Python,
duration: Option<f64>,
error: Option<f64>,
calibration: Option<PyObject>,
) -> Self {
let mut instance = Self {
error,
duration,
_calibration: None,
};
if let Some(calibration) = calibration {
let _ = instance.set_calibration(calibration.into_bound(py));
}
instance
}

fn __getstate__(&self) -> PyResult<(Option<f64>, Option<f64>)> {
Ok((self.duration, self.error))
/// The pulse representation of the instruction.
///
/// .. note::
///
/// This attribute always returns a Qiskit pulse program, but it is internally
/// wrapped by the :class:`.CalibrationEntry` to manage unbound parameters
/// and to uniformly handle different data representation,
/// for example, un-parsed Pulse Qobj JSON that a backend provider may provide.
///
/// This value can be overridden through the property setter in following manner.
/// When you set either :class:`.Schedule` or :class:`.ScheduleBlock` this is
/// always treated as a user-defined (custom) calibration and
/// the transpiler may automatically attach the calibration data to the output circuit.
/// This calibration data may appear in the wire format as an inline calibration,
/// which may further update the backend standard instruction set architecture.
///
/// If you are a backend provider who provides a default calibration data
/// that is not needed to be attached to the transpiled quantum circuit,
/// you can directly set :class:`.CalibrationEntry` instance to this attribute,
/// in which you should set :code:`user_provided=False` when you define
/// calibration data for the entry. End users can still intentionally utilize
/// the calibration data, for example, to run pulse-level simulation of the circuit.
/// However, such entry doesn't appear in the wire format, and backend must
/// use own definition to compile the circuit down to the execution format.
#[getter]
fn get_calibration(&self, py: Python) -> PyResult<PyObject> {
if let Some(calib) = &self._calibration {
Ok(calib.call_method0(py, "get_schedule")?)
} else {
Ok(py.None())
}
}

fn __setstate__(&mut self, _py: Python<'_>, state: (Option<f64>, Option<f64>)) -> PyResult<()> {
self.duration = state.0;
self.error = state.1;
#[setter]
fn set_calibration(&mut self, calibration: Bound<PyAny>) -> PyResult<()> {
let py = calibration.py();
if calibration.is_instance(SCHEDULE.get_bound(py))?
|| calibration.is_instance(SCHEDULE_BLOCK.get_bound(py))?
{
let new_entry = SCHEDULE_DEF.get_bound(py).call0()?;
new_entry.call_method(
"define",
(calibration,),
Some(&[("user_provided", true)].into_py_dict_bound(py)),
)?;
self._calibration = Some(new_entry.into());
} else {
self._calibration = Some(calibration.into());
}
Ok(())
}

fn __repr__(&self, _py: Python<'_>) -> String {
format!(
"InstructionProperties(duration={}, error={})",
if let Some(duration) = self.duration {
duration.to_string()
} else {
"None".to_string()
},
if let Some(error) = self.error {
error.to_string()
} else {
"None".to_string()
}
)
fn __getnewargs__(&self, py: Python) -> PyResult<(Option<f64>, Option<f64>, Option<PyObject>)> {
Ok((
self.duration,
self.error,
self._calibration
.as_ref()
.map(|calibration| calibration.clone_ref(py)),
))
}

fn __repr__(slf: Bound<Self>) -> PyResult<String> {
let duration = slf.getattr("duration")?.str()?.to_string();
let error = slf.getattr("error")?.str()?.to_string();
let calibration = slf.getattr("_calibration")?.str()?.to_string();
Ok(format!(
"InstructionProperties(duration={}, error={}, calibration={})",
duration, error, calibration,
))
}
}
9 changes: 6 additions & 3 deletions crates/accelerate/src/target_transpiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,6 @@ impl Target {
/// properties (InstructionProperties): The properties to set for this instruction
/// Raises:
/// KeyError: If ``instruction`` or ``qarg`` are not in the target
#[pyo3(text_signature = "(instruction, qargs, properties, /,)")]
fn update_instruction_properties(
&mut self,
instruction: &str,
Expand Down Expand Up @@ -774,8 +773,12 @@ impl Target {
/// Get the operation names in the target.
#[getter]
#[pyo3(name = "operation_names")]
fn py_operation_names(&self, py: Python<'_>) -> Py<PyList> {
PyList::new_bound(py, self.operation_names()).unbind()
fn py_operation_names(&self, py: Python<'_>) -> PyResult<PyObject> {
let null_dict = PyDict::new_bound(py);
for name in self.operation_names() {
null_dict.set_item(name, py.None())?;
}
Ok(null_dict.into_any().call_method0("keys")?.into())
}

/// Get the operation objects in the target.
Expand Down
71 changes: 8 additions & 63 deletions qiskit/transpiler/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,26 +71,28 @@ class InstructionProperties(BaseInstructionProperties):
custom attributes for those custom/additional properties by the backend.
"""

__slots__ = [
"_calibration",
]

def __new__( # pylint: disable=keyword-arg-before-vararg
cls,
duration=None, # pylint: disable=keyword-arg-before-vararg
error=None, # pylint: disable=keyword-arg-before-vararg
calibration=None,
*args, # pylint: disable=unused-argument
**kwargs, # pylint: disable=unused-argument
):
return super(InstructionProperties, cls).__new__( # pylint: disable=too-many-function-args
cls, duration, error
cls,
duration,
error,
calibration,
)

def __init__(
self,
duration: float | None = None, # pylint: disable=unused-argument
error: float | None = None, # pylint: disable=unused-argument
calibration: Schedule | ScheduleBlock | CalibrationEntry | None = None,
calibration: ( # pylint: disable=unused-argument
Schedule | ScheduleBlock | CalibrationEntry | None
) = None,
):
"""Create a new ``InstructionProperties`` object
Expand All @@ -102,63 +104,6 @@ def __init__(
calibration: The pulse representation of the instruction.
"""
super().__init__()
self._calibration: CalibrationEntry | None = None
self.calibration = calibration

@property
def calibration(self):
"""The pulse representation of the instruction.
.. note::
This attribute always returns a Qiskit pulse program, but it is internally
wrapped by the :class:`.CalibrationEntry` to manage unbound parameters
and to uniformly handle different data representation,
for example, un-parsed Pulse Qobj JSON that a backend provider may provide.
This value can be overridden through the property setter in following manner.
When you set either :class:`.Schedule` or :class:`.ScheduleBlock` this is
always treated as a user-defined (custom) calibration and
the transpiler may automatically attach the calibration data to the output circuit.
This calibration data may appear in the wire format as an inline calibration,
which may further update the backend standard instruction set architecture.
If you are a backend provider who provides a default calibration data
that is not needed to be attached to the transpiled quantum circuit,
you can directly set :class:`.CalibrationEntry` instance to this attribute,
in which you should set :code:`user_provided=False` when you define
calibration data for the entry. End users can still intentionally utilize
the calibration data, for example, to run pulse-level simulation of the circuit.
However, such entry doesn't appear in the wire format, and backend must
use own definition to compile the circuit down to the execution format.
"""
if self._calibration is None:
return None
return self._calibration.get_schedule()

@calibration.setter
def calibration(self, calibration: Schedule | ScheduleBlock | CalibrationEntry):
if isinstance(calibration, (Schedule, ScheduleBlock)):
new_entry = ScheduleDef()
new_entry.define(calibration, user_provided=True)
else:
new_entry = calibration
self._calibration = new_entry

def __repr__(self):
return (
f"InstructionProperties(duration={self.duration}, error={self.error}"
f", calibration={self._calibration})"
)

def __getstate__(self) -> tuple:
return (super().__getstate__(), self.calibration, self._calibration)

def __setstate__(self, state: tuple):
super().__setstate__(state[0])
self.calibration = state[1]
self._calibration = state[2]


class Target(BaseTarget):
Expand Down

0 comments on commit 9d627f7

Please sign in to comment.