From 52b9d37f17762bd687be2e9eaed471939655b7a6 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Wed, 1 May 2019 17:16:36 -0400 Subject: [PATCH] Pulse command definition (#2263) * Add pulse backend configuration. * Add QASM and Pulse backend configuration objects. * Added QASMBackendConfiguration and PulseBackendConfiguration with factory method from_dict. * add tests for qasm/pulse backend configuration. * linting. * Parameter update * Cleanup provider models and rename. * linting * Linting * Make QASM -> Qasm * remove invalid name * Update open_pulse field to required. * Remove from_dict hackery. * Update tests. * linting. * linting. * linting and required variable. * readd comment. * Add setting of u_channel_lo frequency. * Redo qubit signature, all control channels present. * linting * Remove lo_freq from pulse channels. * linting. * Rework the way lo configuration works. Removed from channels and instead responsibility is placed on assembler. * Small change to trigger rebuild. * Fix rebasing. * Add buffering of commands. * Add buffer tests. * linting. * Small bug in device specification fall back. * Add buffer to acquire channel. * Added command definition data structure. * Added casting of integer input qubits to tuple for cmd_def. * Added cmd_def object tests. Not running as proper pulse schedule api is not yet running. * Update imports and making pulse library. * Added cmd_def and pulse_library to defaults in pulse mock backend * Remove duplicate of PulseLibraryItem. * remove redefinition of pulse qobj schema. * linting * update schema for parameterizable pulses. * Add mutable mapping * Remove mutablemapping and dunder methods. * cmd_def pulse_library defaults methods. * Add cmd_def accesor method. * Update schema for vals and phase of type string. * Added parameterized schedule and typing to cmd_def. * Update typing of CmdDef. Add cmd_qubits method. * Make ConversionMethodBinder generic. * Rename PulseQobjConverter to InstructionToQobjConverter * Add conversion logic for qobj to schedules. * Convert without built pulse_library. * QobjToInstruction tests and passing. * Small bug fix. * Fix cmd_def tests. * linting. * linting. * add linting. * Add safe expression parsing. * Add Parameterized Schedule logic. * Add parameterized schedules with tests. * Fix cmd_def test. * Working cmd def. * Added parameterized cmd_def test * Update tests. * Tests passing. * change cmd_types to cmds) * Update cmd_def errors. * Update error strings. * Fix cmd_qubits. * Linting. * update regex and docs. * Add testing of sanitization * Update sanitization testing. * linting. * Update imports. * Undo standard imports. * Fix bug in pulse * Escape regex --- qiskit/assembler/assemble_schedules.py | 8 +- qiskit/providers/models/__init__.py | 4 +- qiskit/providers/models/pulsedefaults.py | 42 +-- qiskit/pulse/__init__.py | 2 + qiskit/pulse/cmd_def.py | 192 +++++++++++ qiskit/pulse/commands/sample_pulse.py | 4 +- qiskit/pulse/pulse_lib/continuous.py | 2 +- qiskit/pulse/schedule.py | 63 +++- qiskit/qobj/__init__.py | 3 +- qiskit/qobj/converters/__init__.py | 2 +- qiskit/qobj/converters/pulse_instruction.py | 350 +++++++++++++++++--- qiskit/qobj/models/pulse.py | 23 +- qiskit/schemas/qobj_schema.json | 12 +- qiskit/test/mock.py | 35 +- test/python/pulse/test_cmd_def.py | 100 ++++++ test/python/qobj/test_pulse_converter.py | 157 ++++++++- test/python/qobj/test_qobj.py | 10 +- 17 files changed, 887 insertions(+), 122 deletions(-) create mode 100644 qiskit/pulse/cmd_def.py create mode 100644 test/python/pulse/test_cmd_def.py diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py index 6f4d3d2198..641c645a02 100644 --- a/qiskit/assembler/assemble_schedules.py +++ b/qiskit/assembler/assemble_schedules.py @@ -19,8 +19,8 @@ from qiskit.exceptions import QiskitError from qiskit.pulse.commands import PulseInstruction from qiskit.qobj import (PulseQobj, QobjExperimentHeader, PulseQobjInstruction, PulseQobjExperimentConfig, - PulseQobjExperiment, PulseQobjConfig, QobjPulseLibrary) -from qiskit.qobj.converters import PulseQobjConverter, LoConfigConverter + PulseQobjExperiment, PulseQobjConfig, PulseLibraryItem) +from qiskit.qobj.converters import InstructionToQobjConverter, LoConfigConverter logger = logging.getLogger(__name__) @@ -40,7 +40,7 @@ def assemble_schedules(schedules, qobj_id=None, qobj_header=None, run_config=Non if hasattr(run_config, 'instruction_converter'): instruction_converter = run_config.instruction_converter else: - instruction_converter = PulseQobjConverter + instruction_converter = InstructionToQobjConverter qobj_config = run_config.to_dict() qubit_lo_range = qobj_config.pop('qubit_lo_range') @@ -75,7 +75,7 @@ def assemble_schedules(schedules, qobj_id=None, qobj_header=None, run_config=Non }) # setup pulse_library - qobj_config['pulse_library'] = [QobjPulseLibrary(name=pulse.name, samples=pulse.samples) + qobj_config['pulse_library'] = [PulseLibraryItem(name=pulse.name, samples=pulse.samples) for pulse in user_pulselib] # create qob experiment field diff --git a/qiskit/providers/models/__init__.py b/qiskit/providers/models/__init__.py index 121238b764..1e77d5e499 100644 --- a/qiskit/providers/models/__init__.py +++ b/qiskit/providers/models/__init__.py @@ -15,8 +15,8 @@ """Qiskit schema-conformant objects used by the backends and providers.""" from .backendconfiguration import (BackendConfiguration, PulseBackendConfiguration, - QasmBackendConfiguration, UchannelLO) + QasmBackendConfiguration, UchannelLO, GateConfig) from .backendproperties import BackendProperties from .backendstatus import BackendStatus from .jobstatus import JobStatus -from .pulsedefaults import PulseDefaults +from .pulsedefaults import PulseDefaults, Command diff --git a/qiskit/providers/models/pulsedefaults.py b/qiskit/providers/models/pulsedefaults.py index 402fbc35cc..efc1927f90 100644 --- a/qiskit/providers/models/pulsedefaults.py +++ b/qiskit/providers/models/pulsedefaults.py @@ -11,17 +11,9 @@ from marshmallow.validate import Length, Range from qiskit.validation import BaseModel, BaseSchema, bind_schema from qiskit.validation.base import ObjSchema -from qiskit.validation.fields import (Complex, Integer, List, Nested, Number, - String) - - -class PulseLibraryItemSchema(BaseSchema): - """Schema for PulseLibraryItem.""" - - # Required properties. - name = String(required=True) - samples = List(Complex(), required=True, - validate=Length(min=1)) +from qiskit.validation.fields import (Integer, List, Nested, Number, String) +from qiskit.qobj import PulseLibraryItemSchema, PulseQobjInstructionSchema +from qiskit.pulse import CmdDef class MeasurementKernelSchema(BaseSchema): @@ -49,7 +41,7 @@ class CommandSchema(BaseSchema): # Optional properties. qubits = List(Integer(validate=Range(min=0)), validate=Length(min=1)) - sequence = Nested(ObjSchema, many=True) + sequence = Nested(PulseQobjInstructionSchema, many=True) class PulseDefaultsSchema(BaseSchema): @@ -67,24 +59,6 @@ class PulseDefaultsSchema(BaseSchema): discriminator = Nested(DiscriminatorSchema) -@bind_schema(PulseLibraryItemSchema) -class PulseLibraryItem(BaseModel): - """Model for PulseLibraryItem. - - Please note that this class only describes the required fields. For the - full description of the model, please check ``PulseLibraryItemSchema``. - - Attributes: - name (str): Pulse name. - samples (list[complex]): Pulse samples. - """ - def __init__(self, name, samples, **kwargs): - self.name = name - self.samples = samples - - super().__init__(**kwargs) - - @bind_schema(MeasurementKernelSchema) class MeasurementKernel(BaseModel): """Model for MeasurementKernel. @@ -146,3 +120,11 @@ class PulseDefaults(BaseModel): self.cmd_def = cmd_def super().__init__(**kwargs) + + def build_cmd_def(self): + """Construct the `CmdDef` object for the backend. + + Returns: + CmdDef: `CmdDef` instance generated from defaults + """ + return CmdDef.from_defaults(self.cmd_def, self.pulse_library) diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index faa9de800d..ab469b9824 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -19,3 +19,5 @@ from .commands import (Acquire, FrameChange, PersistentValue, SamplePulse, Snaps Kernel, Discriminator, functional_pulse) from .configuration import LoConfig, LoRange from .schedule import Schedule +from .cmd_def import CmdDef +from .exceptions import PulseError diff --git a/qiskit/pulse/cmd_def.py b/qiskit/pulse/cmd_def.py new file mode 100644 index 0000000000..d9088a923f --- /dev/null +++ b/qiskit/pulse/cmd_def.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- + +# Copyright 2019, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +""" +Command definition module. Relates circuit gates to pulse commands. +""" +from typing import List, Tuple, Iterable, Union, Dict + +from qiskit.qobj import PulseQobjInstruction +from qiskit.qobj.converters import QobjToInstructionConverter + +from .commands import SamplePulse + +from .exceptions import PulseError +from .schedule import Schedule, ParameterizedSchedule + +# pylint: disable=missing-return-doc + + +def _to_qubit_tuple(qubit_tuple: Union[int, Iterable[int]]) -> Tuple[int]: + """Convert argument to tuple. + Args: + qubit_tuple: Qubits to enforce as tuple. + + Raises: + PulseError: If qubits are not integers + """ + try: + qubit_tuple = tuple(qubit_tuple) + except TypeError: + qubit_tuple = (qubit_tuple,) + + if not all(isinstance(i, int) for i in qubit_tuple): + raise PulseError("All qubits must be integers.") + + return qubit_tuple + + +class CmdDef: + """Command definition class. Relates `Gate`s to `Schedule`s.""" + + def __init__(self, schedules: Dict = None): + """Create command definition from backend. + + Args: + schedules: Keys are tuples of (cmd_name, *qubits) and values are + `Schedule` or `ParameterizedSchedule` + """ + self._cmd_dict = {} + + if schedules: + for key, schedule in schedules.items(): + self.add(key[0], key[1:], schedule) + + @classmethod + def from_defaults(cls, flat_cmd_def: List[PulseQobjInstruction], + pulse_library: Dict[str, SamplePulse]) -> 'CmdDef': + """Create command definition from backend defaults output. + Args: + flat_cmd_def: Command definition list returned by backend + pulse_library: Dictionary of `SamplePulse`s + """ + converter = QobjToInstructionConverter(pulse_library, buffer=0) + cmd_def = cls() + + for cmd in flat_cmd_def: + qubits = cmd.qubits + name = cmd.name + instructions = [] + for instr in cmd.sequence: + instructions.append(converter(instr)) + + cmd_def.add(name, qubits, ParameterizedSchedule(*instructions, name=name)) + + return cmd_def + + def add(self, cmd_name: str, qubits: Union[int, Iterable[int]], + schedule: Union[ParameterizedSchedule, Schedule]): + """Add a command to the `CommandDefinition` + + Args: + cmd_name: Name of the command + qubits: Qubits command applies to + schedule: Schedule to be added + """ + qubits = _to_qubit_tuple(qubits) + cmd_dict = self._cmd_dict.setdefault(cmd_name, {}) + if isinstance(schedule, Schedule): + schedule = ParameterizedSchedule(schedule, name=schedule.name) + cmd_dict[qubits] = schedule + + def has(self, cmd_name: str, qubits: Union[int, Iterable[int]]) -> bool: + """Has command of name with qubits. + + Args: + cmd_name: Name of the command + qubits: Ordered list of qubits command applies to + """ + qubits = _to_qubit_tuple(qubits) + if cmd_name in self._cmd_dict: + + if qubits in self._cmd_dict[cmd_name]: + return True + + return False + + def get(self, cmd_name: str, qubits: Union[int, Iterable[int]], + **params: Dict[str, Union[float, complex]]) -> Schedule: + """Get command from command definition. + Args: + cmd_name: Name of the command + qubits: Ordered list of qubits command applies to + **params: Command parameters to be used to generate schedule + + Raises: + PulseError: If command for qubits is not available + """ + qubits = _to_qubit_tuple(qubits) + if self.has(cmd_name, qubits): + schedule = self._cmd_dict[cmd_name][qubits] + + if isinstance(schedule, ParameterizedSchedule): + return schedule.bind_parameters(**params) + + return schedule.flatten() + + else: + raise PulseError('Command {0} for qubits {1} is not present ' + 'in CmdDef'.format(cmd_name, qubits)) + + def get_parameters(self, cmd_name: str, qubits: Union[int, Iterable[int]]) -> Tuple[str]: + """Get command parameters from command definition. + Args: + cmd_name: Name of the command + qubits: Ordered list of qubits command applies to + + Raises: + PulseError: If command for qubits is not available + """ + qubits = _to_qubit_tuple(qubits) + if self.has(cmd_name, qubits): + schedule = self._cmd_dict[cmd_name][qubits] + return schedule.parameters + + else: + raise PulseError('Command {0} for qubits {1} is not present ' + 'in CmdDef'.format(cmd_name, qubits)) + + def pop(self, cmd_name: str, qubits: Union[int, Iterable[int]], + **params: Dict[str, Union[float, complex]]) -> Schedule: + """Pop command from command definition. + + Args: + cmd_name: Name of the command + qubits: Ordered list of qubits command applies to + **params: Command parameters to be used to generate schedule + + Raises: + PulseError: If command for qubits is not available + """ + qubits = _to_qubit_tuple(qubits) + if self.has(cmd_name, qubits): + cmd_dict = self._cmd_dict[cmd_name] + schedule = cmd_dict.pop(qubits) + + if isinstance(schedule, ParameterizedSchedule): + return schedule.bind_parameters(**params) + + return schedule + + else: + raise PulseError('Command {0} for qubits {1} is not present ' + 'in CmdDef'.format(cmd_name, qubits)) + + def cmds(self) -> List[str]: + """Return all command names available in CmdDef.""" + + return list(self._cmd_dict.keys()) + + def cmd_qubits(self, cmd_name: str) -> List[Tuple[int]]: + """Get all qubit orderings this command exists for.""" + if cmd_name in self._cmd_dict: + return list(sorted(self._cmd_dict[cmd_name].keys())) + + raise PulseError('Command %s does not exist in CmdDef.' % cmd_name) + + def __repr__(self): + return repr(self._cmd_dict) diff --git a/qiskit/pulse/commands/sample_pulse.py b/qiskit/pulse/commands/sample_pulse.py index 84433c897f..b9d9f1a551 100644 --- a/qiskit/pulse/commands/sample_pulse.py +++ b/qiskit/pulse/commands/sample_pulse.py @@ -23,7 +23,7 @@ class SamplePulse(Command): """Create new sample pulse command. Args: - samples (ndarray): Complex array of pulse envelope. + samples (Union[np.ndarray, List[Complex]]): Complex array of pulse envelope. name (str): Unique name to identify the pulse. Raises: PulseError: when pulse envelope amplitude exceeds 1. @@ -33,7 +33,7 @@ class SamplePulse(Command): if np.any(np.abs(samples) > 1): raise PulseError('Absolute value of pulse envelope amplitude exceeds 1.') - self._samples = samples + self._samples = np.asarray(samples, dtype=np.complex_) @property def samples(self): diff --git a/qiskit/pulse/pulse_lib/continuous.py b/qiskit/pulse/pulse_lib/continuous.py index e1ecbce55f..59923096cc 100644 --- a/qiskit/pulse/pulse_lib/continuous.py +++ b/qiskit/pulse/pulse_lib/continuous.py @@ -121,7 +121,7 @@ def _fix_gaussian_width(gaussian_samples, amp: float, center: float, sigma: floa gaussian_samples -= zero_offset amp_scale_factor = 1. if rescale_amp: - amp_scale_factor = amp/(amp-zero_offset) + amp_scale_factor = amp/(amp-zero_offset) if amp-zero_offset != 0 else 1. gaussian_samples *= amp_scale_factor if ret_scale_factor: diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py index d4300035d2..a84697c4ab 100644 --- a/qiskit/pulse/schedule.py +++ b/qiskit/pulse/schedule.py @@ -11,15 +11,12 @@ # Any modifications or derivative works of this code must retain this # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. - -# pylint: disable=missing-return-doc,cyclic-import - """ Schedule. """ import itertools import logging -from typing import List, Tuple, Iterable, Union +from typing import List, Tuple, Iterable, Union, Dict from qiskit.pulse import ops from .channels import Channel @@ -29,6 +26,8 @@ from .exceptions import PulseError logger = logging.getLogger(__name__) +# pylint: disable=missing-return-doc,cyclic-import + class Schedule(ScheduleComponent): """Schedule of `ScheduleComponent`s. The composite node of a schedule tree.""" @@ -210,3 +209,59 @@ class Schedule(ScheduleComponent): if len(instructions) > 50: return res + ', ...)' return res + ')' + + +class ParameterizedSchedule: + """Temporary parameterized schedule class. + + This should not be returned to users as it is currently only a helper class. + + This class is takes an input command definition that accepts + a set of parameters. Calling `bind` on the class will return a `Schedule`. + + # TODO: In the near future this will be replaced with proper incorporation of parameters + into the `Schedule` class. + """ + + def __init__(self, *schedules, parameters=None, name=None): + full_schedules = [] + parameterized = [] + parameters = parameters or [] + self.name = name or '' + # partition schedules into callable and schedules + for schedule in schedules: + if isinstance(schedule, ParameterizedSchedule): + parameterized.append(schedule) + parameters += schedule.parameters + elif callable(schedule): + parameterized.append(schedule) + elif isinstance(schedule, Schedule): + full_schedules.append(schedule) + else: + raise PulseError('Input type: {0} not supported'.format(type(schedule))) + + self._parameterized = tuple(parameterized) + self._schedules = tuple(full_schedules) + self._parameters = tuple(sorted(parameters)) + + @property + def parameters(self) -> Tuple[str]: + """Schedule parameters.""" + return self._parameters + + def bind_parameters(self, *args: List[float], **kwargs: Dict[str, float]) -> Schedule: + """Generate the Schedule from params to evaluate command expressions""" + bound_schedule = Schedule(name=self.name) + schedules = list(self._schedules) + for param_sched in self._parameterized: + # recursively call until based callable is reached + schedules.append(param_sched(*args, **kwargs)) + + # construct evaluated schedules + for sched in schedules: + bound_schedule |= sched + + return bound_schedule + + def __call__(self, *args: List[float], **kwargs: Dict[str, float]) -> Schedule: + return self.bind_parameters(*args, **kwargs) diff --git a/qiskit/qobj/__init__.py b/qiskit/qobj/__init__.py index c2f0e9acca..165a7c6c47 100644 --- a/qiskit/qobj/__init__.py +++ b/qiskit/qobj/__init__.py @@ -19,7 +19,8 @@ from .models.base import (QobjInstruction, QobjExperimentHeader, QobjExperimentC from .models.pulse import (PulseQobjInstruction, PulseQobjExperimentConfig, PulseQobjExperiment, PulseQobjConfig, - QobjMeasurementOption, QobjPulseLibrary) + QobjMeasurementOption, PulseLibraryItem, + PulseLibraryItemSchema, PulseQobjInstructionSchema) from .models.qasm import (QasmQobjInstruction, QasmQobjExperimentConfig, QasmQobjExperiment, QasmQobjConfig) diff --git a/qiskit/qobj/converters/__init__.py b/qiskit/qobj/converters/__init__.py index 9f5e5eaf0a..6077a1de44 100644 --- a/qiskit/qobj/converters/__init__.py +++ b/qiskit/qobj/converters/__init__.py @@ -9,5 +9,5 @@ Helper modules to convert qiskit frontend object to proper qobj model. """ -from .pulse_instruction import PulseQobjConverter +from .pulse_instruction import InstructionToQobjConverter, QobjToInstructionConverter from .lo_config import LoConfigConverter diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 9a6ddd0841..5c9f1f471b 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -6,77 +6,67 @@ # the LICENSE.txt file in the root directory of this source tree. """Helper class used to convert a pulse instruction into PulseQobjInstruction.""" +import re +import math -import functools +from sympy.parsing.sympy_parser import (parse_expr, standard_transformations, + implicit_multiplication_application, + function_exponentiation) +from sympy import Symbol -from qiskit.pulse import commands +from qiskit.pulse import commands, channels, Schedule +from qiskit.pulse.schedule import ParameterizedSchedule from qiskit.pulse.exceptions import PulseError from qiskit.qobj import QobjMeasurementOption +from qiskit import QiskitError class ConversionMethodBinder: - """Instruction conversion method registrar.""" + """Conversion method registrar.""" def __init__(self): - """Acts as method registration decorator and tracker for instruction conversion methods.""" + """Acts as method registration decorator and tracker for conversion methods.""" self._bound_instructions = {} - def __call__(self, type_instruction): + def __call__(self, bound): """ Converter decorator method. - Pulse instruction converter is defined for each instruction type, - and this decorator binds converter function to valid instruction type. + Converter is defined for object to be converted matched on hash Args: - type_instruction (Instruction): valid pulse instruction class to the converter. + bound (Hashable): Hashable object to bind to the converter. """ # pylint: disable=missing-return-doc, missing-return-type-doc def _apply_converter(converter): """Return decorated converter function.""" - - @functools.wraps(converter) - def _call_valid_converter(self, shift, instruction): - """Return a dictionary for to be used to construct a qobj - if the given instruction matches the - bound instruction type supplied to the function, - otherwise return None.""" - if isinstance(instruction, type_instruction): - return converter(self, shift, instruction) - else: - raise PulseError('Supplied instruction {0} ' - 'is not of type {1}.'.format(instruction, type_instruction)) - # Track conversion methods for class. - self._bound_instructions[type_instruction] = _call_valid_converter - return _call_valid_converter + self._bound_instructions[bound] = converter + return converter return _apply_converter - def get_bound_method(self, instruction): - """Get conversion method for instruction.""" + def get_bound_method(self, bound): + """Get conversion method for bound object.""" try: - return self._bound_instructions[type(instruction)] + return self._bound_instructions[bound] except KeyError: - raise PulseError('Qobj conversion method for %s is not found.' % instruction) + raise PulseError('Bound method for %s is not found.' % bound) -class PulseQobjConverter: - """ - This class exists for separating entity of pulse instructions and qobj instruction, - and provides some alleviation of the complexity of the assembler. +class InstructionToQobjConverter: + """Converts pulse Instructions to Qobj models. - Converter is constructed with qobj model and some experimental configuration, + Converter is constructed with qobj model and experimental configuration, and returns proper qobj instruction to each backend. - Pulse instruction and its qobj are strongly depends on the design of backend, - and third party providers can be easily add their custom pulse instruction by - providing custom converter inherit from this. + Third party providers can be add their own custom pulse instructions by + providing custom converter methods. - To create custom converter for custom instruction + To create a custom converter for custom instruction ``` - class CustomConverter(PulseQobjConverter): + class CustomConverter(InstructionToQobjConverter): @bind_instruction(CustomInstruction) def convert_custom_command(self, shift, instruction): @@ -108,7 +98,7 @@ class PulseQobjConverter: def __call__(self, shift, instruction): - method = self.bind_instruction.get_bound_method(instruction) + method = self.bind_instruction.get_bound_method(type(instruction)) return method(self, shift, instruction) @bind_instruction(commands.AcquireInstruction) @@ -226,3 +216,287 @@ class PulseQobjConverter: 'type': instruction.type } return self._qobj_model(**command_dict) + + +# pylint: disable=invalid-name + +# get math operations valid in python. Presumably these are valid in sympy +_math_ops = [math_op for math_op in math.__dict__ if not math_op.startswith('__')] +# only allow valid math ops +_math_ops_regex = r"(" + ")|(".join(_math_ops) + ")" +# match consecutive alphanumeric, and single consecutive math ops +-/.() +# and multiple * for exponentiation +_allowedchars = re.compile(r'([+\/\-\(\)\.])?([\sa-zA-Z\d]+[+\/\-\(\)\.]?\*{0,2})*') +# match any sequence of chars and numbers +_expr_regex = r'([a-zA-Z]+\d*)' +# and valid params +_param_regex = r'(P\d+)' +# only valid sequences are P# for parameters and valid math operations above +_valid_sub_expr = re.compile(_param_regex+'|'+_math_ops_regex) +# pylint: enable=invalid-name + + +def _is_math_expr_safe(expr): + r"""Verify mathematical expression is sanitized. + + Only allow strings of form 'P\d+' and operations from `math`. + Allowed chars are [a-zA-Z]. Allowed math operators are '+*/().' + where only '*' are allowed to be consecutive. + + Args: + expr (str): Expression to sanitize + + Returns: + bool: Whether the string is safe to parse math from + + Raise: + QiskitError: If math expression is not sanitized + """ + + only_allowed_chars = _allowedchars.match(expr) + if not only_allowed_chars: + return False + elif not only_allowed_chars.group(0) == expr: + return False + + sub_expressions = re.findall(_expr_regex, expr) + if not all([_valid_sub_expr.match(sub_exp) for sub_exp in sub_expressions]): + return False + return True + + +def _parse_string_expr(expr): # pylint: disable=missing-return-type-doc + """Parse a mathematical string expression and extract free parameters. + + Args: + expr (str): String expression to parse + + Returns: + (Callable, Tuple[str]): Returns a callable function and tuple of string symbols + + Raises: + QiskitError: If expression is not safe + """ + # remove these strings from expression and hope sympy knows these math expressions + # these are effectively reserved keywords + subs = [('numpy.', ''), ('np.', ''), ('math.', '')] + for match, sub in subs: + expr = expr.replace(match, sub) + if not _is_math_expr_safe(expr): + raise QiskitError('Expression: "%s" is not safe to evaluate.' % expr) + params = sorted(re.findall(_param_regex, expr)) + local_dict = {param: Symbol(param) for param in params} + symbols = list(local_dict.keys()) + transformations = (standard_transformations + (implicit_multiplication_application,) + + (function_exponentiation,)) + + parsed_expr = parse_expr(expr, local_dict=local_dict, transformations=transformations) + + def parsed_fun(*args, **kwargs): + subs = {} + matched_params = [] + if args: + subs.update({symbols[i]: arg for i, arg in enumerate(args)}) + matched_params += list(params[i] for i in range(len(args))) + elif kwargs: + subs.update({local_dict[key]: value for key, value in kwargs.items() + if key in local_dict}) + matched_params += list(key for key in kwargs if key in params) + + if not set(matched_params).issuperset(set(params)): + raise PulseError('Supplied params ({args}, {kwargs}) do not match ' + '{params}'.format(args=args, kwargs=kwargs, params=params)) + + return complex(parsed_expr.evalf(subs=subs)) + return parsed_fun, params + + +class QobjToInstructionConverter: + """Converts Qobj models to pulse Instructions + """ + # pylint: disable=invalid-name,missing-return-type-doc + # class level tracking of conversion methods + bind_name = ConversionMethodBinder() + chan_regex = re.compile(r'([a-zA-Z]+)(\d+)') + + def __init__(self, pulse_library, buffer=0, **run_config): + """Create new converter. + + Args: + pulse_library (List[PulseLibraryItem]): Pulse library to be used in conversion + buffer (int): Channel buffer + run_config (dict): experimental configuration. + """ + self.buffer = buffer + self._run_config = run_config + + # bind pulses to conversion methods + for pulse in pulse_library: + self.bind_pulse(pulse) + + def __call__(self, instruction): + + method = self.bind_name.get_bound_method(instruction.name) + return method(self, instruction) + + def get_channel(self, channel): + """Parse and retrieve channel from ch string. + + Args: + channel (str): Channel to match + + Returns: + (Channel, int): Matched channel + + Raises: + PulseError: Is raised if valid channel is not matched + """ + match = self.chan_regex.match(channel) + if match: + prefix, index = match.group(1), int(match.group(2)) + + if prefix == channels.DriveChannel.prefix: + return channels.DriveChannel(index, buffer=self.buffer) + elif prefix == channels.MeasureChannel.prefix: + return channels.MeasureChannel(index, buffer=self.buffer) + elif prefix == channels.ControlChannel.prefix: + return channels.ControlChannel(index, buffer=self.buffer) + + raise PulseError('Channel %s is not valid' % channel) + + @bind_name('acquire') + def convert_acquire(self, instruction): + """Return converted `AcquireInstruction`. + + Args: + instruction (PulseQobjInstruction): acquire qobj + Returns: + Schedule: Converted and scheduled Instruction + """ + t0 = instruction.t0 + duration = instruction.duration + qubits = instruction.qubits + discriminators = (instruction.discriminators + if hasattr(instruction, 'discriminators') else None) + + kernels = (instruction.kernels + if hasattr(instruction, 'kernels') else None) + + mem_slots = instruction.memory_slot + reg_slots = (instruction.register_slot + if hasattr(instruction, 'register_slot') else None) + + if not isinstance(discriminators, list): + discriminators = [discriminators for _ in range(len(qubits))] + + if not isinstance(kernels, list): + kernels = [kernels for _ in range(len(qubits))] + + schedule = Schedule() + + for i, qubit in enumerate(qubits): + kernel = kernels[i] + if kernel: + kernel = commands.Kernel(name=kernel.name, + params=kernel.params) + + discriminator = discriminators[i] + if discriminator: + discriminator = commands.Discriminator(name=discriminator.name, + params=discriminator.params) + + channel = channels.AcquireChannel(qubit, buffer=self.buffer) + if reg_slots: + register_slot = channels.RegisterSlot(reg_slots[i]) + else: + register_slot = None + memory_slot = channels.MemorySlot(mem_slots[i]) + + cmd = commands.Acquire(duration, discriminator=discriminator, kernel=kernel) + schedule |= commands.AcquireInstruction(cmd, channel, memory_slot, register_slot) << t0 + + return schedule + + @bind_name('fc') + def convert_frame_change(self, instruction): + """Return converted `FrameChangeInstruction`. + + Args: + instruction (PulseQobjInstruction): frame change qobj + Returns: + Schedule: Converted and scheduled Instruction + """ + t0 = instruction.t0 + channel = self.get_channel(instruction.ch) + phase = instruction.phase + + # This is parameterized + if isinstance(phase, str): + phase_expr, params = _parse_string_expr(phase) + + def gen_fc_sched(*args, **kwargs): + phase = abs(phase_expr(*args, **kwargs)) + return commands.FrameChange(phase)(channel) << t0 + + return ParameterizedSchedule(gen_fc_sched, parameters=params) + + return commands.FrameChange(phase)(channel) << t0 + + @bind_name('pv') + def convert_persistent_value(self, instruction): + """Return converted `PersistentValueInstruction`. + + Args: + instruction (PulseQobjInstruction): persistent value qobj + Returns: + Schedule: Converted and scheduled Instruction + """ + t0 = instruction.t0 + channel = self.get_channel(instruction.ch) + val = instruction.val + + # This is parameterized + if isinstance(val, str): + val_expr, params = _parse_string_expr(val) + + def gen_fc_sched(*args, **kwargs): + val = complex(val_expr(*args, **kwargs)) + return commands.PersistentValue(val)(channel) << t0 + + return ParameterizedSchedule(gen_fc_sched, parameters=params) + + return commands.PersistentValue(val)(channel) << t0 + + def bind_pulse(self, pulse): + """Bind the supplied pulse to a converter method by pulse name. + + Args: + pulse (PulseLibraryItem): Pulse to bind + """ + # pylint: disable=unused-variable + pulse = commands.SamplePulse(pulse.samples, pulse.name) + + @self.bind_name(pulse.name) + def convert_named_drive(self, instruction): + """Return converted `PulseInstruction`. + + Args: + instruction (PulseQobjInstruction): pulse qobj + Returns: + Schedule: Converted and scheduled pulse + """ + t0 = instruction.t0 + channel = self.get_channel(instruction.ch) + return pulse(channel) << t0 + + @bind_name('snapshot') + def convert_snapshot(self, instruction): + """Return converted `Snapshot`. + + Args: + instruction (PulseQobjInstruction): snapshot qobj + Returns: + Schedule: Converted and scheduled Snapshot + """ + t0 = instruction.t0 + return commands.Snapshot(instruction.label, instruction.type) << t0 diff --git a/qiskit/qobj/models/pulse.py b/qiskit/qobj/models/pulse.py index 44a592a544..d100ce7bd4 100644 --- a/qiskit/qobj/models/pulse.py +++ b/qiskit/qobj/models/pulse.py @@ -11,7 +11,8 @@ from marshmallow.validate import Range, Regexp, Length, OneOf from qiskit.qobj.utils import MeasReturnType from qiskit.validation import BaseSchema, bind_schema, BaseModel -from qiskit.validation.fields import Integer, String, Number, Complex, List, Nested, DictParameters +from qiskit.validation.fields import (Integer, String, Number, Complex, List, + Nested, DictParameters, ByType) from .base import (QobjInstructionSchema, QobjExperimentConfigSchema, QobjExperimentSchema, QobjConfigSchema, QobjInstruction, QobjExperimentConfig, QobjExperiment, QobjConfig) @@ -26,8 +27,8 @@ class QobjMeasurementOptionSchema(BaseSchema): required=True) -class QobjPulseLibrarySchema(BaseSchema): - """Schema for QobjPulseLibrary.""" +class PulseLibraryItemSchema(BaseSchema): + """Schema for PulseLibraryItem.""" # Required properties. name = String(required=True) @@ -44,8 +45,8 @@ class PulseQobjInstructionSchema(QobjInstructionSchema): # Optional properties. ch = String(validate=Regexp('[dum]([0-9])+')) conditional = Integer(validate=Range(min=0)) - phase = Number() - val = Complex() + val = ByType([Complex(), String()]) + phase = ByType([String(), Number()]) duration = Integer(validate=Range(min=1)) qubits = List(Integer(validate=Range(min=0)), validate=Length(min=1)) memory_slot = List(Integer(validate=Range(min=0)), validate=Length(min=1)) @@ -83,7 +84,7 @@ class PulseQobjConfigSchema(QobjConfigSchema): meas_level = Integer(required=True, validate=Range(min=0, max=2)) meas_return = String(required=True, validate=OneOf(choices=(MeasReturnType.AVERAGE, MeasReturnType.SINGLE))) - pulse_library = Nested(QobjPulseLibrarySchema, required=True, many=True) + pulse_library = Nested(PulseLibraryItemSchema, required=True, many=True) qubit_lo_freq = List(Number(validate=Range(min=0)), required=True) meas_lo_freq = List(Number(validate=Range(min=0)), required=True) @@ -110,12 +111,12 @@ class QobjMeasurementOption(BaseModel): super().__init__(**kwargs) -@bind_schema(QobjPulseLibrarySchema) -class QobjPulseLibrary(BaseModel): - """Model for QobjPulseLibrary. +@bind_schema(PulseLibraryItemSchema) +class PulseLibraryItem(BaseModel): + """Model for PulseLibraryItem. Please note that this class only describes the required fields. For the - full description of the model, please check ``QobjPulseLibrarySchema``. + full description of the model, please check ``PulseLibraryItemSchema``. Attributes: name (str): name of pulse @@ -186,7 +187,7 @@ class PulseQobjConfig(QobjConfig): meas_level (int): a value represents the level of measurement. meas_lo_freq (list[float]): local oscillator frequency of measurement pulse. meas_return (str): a level of measurement information. - pulse_library (list[qiskit.qobj.QobjPulseLibrary]): a pulse library. + pulse_library (list[qiskit.qobj.PulseLibraryItem]): a pulse library. qubit_lo_freq (list[float]): local oscillator frequency of driving pulse. """ def __init__(self, meas_level, meas_return, pulse_library, diff --git a/qiskit/schemas/qobj_schema.json b/qiskit/schemas/qobj_schema.json index 97b42c43e4..9a50961d1c 100644 --- a/qiskit/schemas/qobj_schema.json +++ b/qiskit/schemas/qobj_schema.json @@ -115,13 +115,15 @@ ] }, "val": { - "items": { + "OneOf": [ + {"items": { "type": "number" }, "maxItems": 2, "minItems": 2, - "type": "array" - } + "type": "array"}, + {"type": "string"} + ]} }, "required": [ "val", @@ -138,7 +140,9 @@ ] }, "phase": { - "type": "number" + "OneOf":[ + {"type": "number"}, + {"type": "string"}] } }, "required": [ diff --git a/qiskit/test/mock.py b/qiskit/test/mock.py index e9226aaac3..59e860041e 100644 --- a/qiskit/test/mock.py +++ b/qiskit/test/mock.py @@ -31,12 +31,13 @@ import time from qiskit.result import Result from qiskit.providers import BaseBackend, BaseJob -from qiskit.providers.models import (BackendProperties, PulseDefaults, UchannelLO, - QasmBackendConfiguration, PulseBackendConfiguration) -from qiskit.providers.models.backendconfiguration import GateConfig +from qiskit.providers.models import (BackendProperties, GateConfig, + QasmBackendConfiguration, PulseBackendConfiguration, + PulseDefaults, Command, UchannelLO) from qiskit.qobj import (QasmQobj, QobjExperimentHeader, QobjHeader, QasmQobjInstruction, QasmQobjExperimentConfig, - QasmQobjExperiment, QasmQobjConfig) + QasmQobjExperiment, QasmQobjConfig, PulseLibraryItem, + PulseQobjInstruction) from qiskit.providers.jobstatus import JobStatus from qiskit.providers.baseprovider import BaseProvider from qiskit.providers.exceptions import QiskitBackendNotFoundError @@ -60,8 +61,9 @@ class FakeProvider(BaseProvider): # pylint: enable=no-member if not filtered_backends: raise QiskitBackendNotFoundError() - else: - backend = filtered_backends[0] + + backend = filtered_backends[0] + return backend def backends(self, name=None, **kwargs): @@ -73,7 +75,7 @@ class FakeProvider(BaseProvider): FakeMelbourne(), FakeRueschlikon(), FakeTokyo(), - FakeOpenPulse2Q()], + FakeOpenPulse2Q()] super().__init__() @@ -192,8 +194,23 @@ class FakeOpenPulse2Q(FakeBackend): qubit_freq_est=[4.9, 5.0], meas_freq_est=[6.5, 6.6], buffer=10, - pulse_library=[], - cmd_def=[] + pulse_library=[PulseLibraryItem(name='test_pulse_1', samples=[0.j, 0.1j]), + PulseLibraryItem(name='test_pulse_2', samples=[0.j, 0.1j, 1j])], + cmd_def=[Command(name='u1', qubits=[0], + sequence=[PulseQobjInstruction(name='fc', ch='d0', + t0=0, phase='-P1*np.pi')]), + Command(name='cx', qubits=[0, 1], + sequence=[PulseQobjInstruction(name='test_pulse_1', ch='d0', t0=0), + PulseQobjInstruction(name='test_pulse_2', ch='u0', t0=10), + PulseQobjInstruction(name='pv', ch='d1', + t0=2, val='cos(P2)'), + PulseQobjInstruction(name='test_pulse_1', ch='d1', t0=20), + PulseQobjInstruction(name='fc', ch='d1', + t0=20, phase=2.1)]), + Command(name='measure', qubits=[0], + sequence=[PulseQobjInstruction(name='test_pulse_1', ch='m0', t0=0), + PulseQobjInstruction(name='acquire', duration=10, t0=0, + qubits=[0], memory_slot=[0])])] ) super().__init__(configuration) diff --git a/test/python/pulse/test_cmd_def.py b/test/python/pulse/test_cmd_def.py new file mode 100644 index 0000000000..4f6e054620 --- /dev/null +++ b/test/python/pulse/test_cmd_def.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +# Copyright 2019, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +"""Test for the CmdDef object.""" + +import numpy as np + +from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeProvider +from qiskit.qobj.converters import QobjToInstructionConverter +from qiskit.qobj import PulseQobjInstruction +from qiskit.pulse import (CmdDef, SamplePulse, Schedule, DeviceSpecification, + PulseError, PersistentValue) + + +class TestCmdDef(QiskitTestCase): + """Test CmdDef methods.""" + + def setUp(self): + self.provider = FakeProvider() + self.backend = self.provider.get_backend('fake_openpulse_2q') + self.device = DeviceSpecification.create_from(self.backend) + + def test_get_backend(self): + """Test that backend is fetchable with cmd def present.""" + + def test_init(self): + """Test `init`, `has`.""" + sched = Schedule() + sched.append(SamplePulse(np.ones(5))(self.device.q[0].drive)) + cmd_def = CmdDef({('tmp', 0): sched}) + self.assertTrue(cmd_def.has('tmp', 0)) + + def test_add(self): + """Test `add`, `has`, `get`, `cmdss`.""" + sched = Schedule() + sched.append(SamplePulse(np.ones(5))(self.device.q[0].drive)) + cmd_def = CmdDef() + cmd_def.add('tmp', 1, sched) + cmd_def.add('tmp', 0, sched) + self.assertEqual(sched.instructions, cmd_def.get('tmp', (0,)).instructions) + + self.assertIn('tmp', cmd_def.cmds()) + self.assertEqual(cmd_def.cmd_qubits('tmp'), [(0,), (1,)]) + + def test_pop(self): + """Test pop with default.""" + sched = Schedule() + sched.append(SamplePulse(np.ones(5))(self.device.q[0].drive)) + cmd_def = CmdDef() + cmd_def.add('tmp', 0, sched) + cmd_def.pop('tmp', 0) + self.assertFalse(cmd_def.has('tmp', 0)) + + with self.assertRaises(PulseError): + cmd_def.pop('not_there', (0,)) + + def test_repr(self): + """Test repr.""" + sched = Schedule() + sched.append(SamplePulse(np.ones(5))(self.device.q[0].drive)) + cmd_def = CmdDef({('tmp', 0): sched}) + repr(cmd_def) + + def test_parameterized_schedule(self): + """Test building parameterized schedule.""" + cmd_def = CmdDef() + converter = QobjToInstructionConverter([], buffer=0) + qobj = PulseQobjInstruction(name='pv', ch='u1', t0=10, val='P2*cos(np.pi*P1)') + converted_instruction = converter(qobj) + + cmd_def.add('pv_test', 0, converted_instruction) + self.assertEqual(cmd_def.get_parameters('pv_test', 0), ('P1', 'P2')) + + sched = cmd_def.get('pv_test', 0, P1='0', P2=-1) + self.assertEqual(sched.instructions[0][-1].command.value, -1) + + def test_build_cmd_def(self): + """Test building of parameterized cmd_def from defaults.""" + defaults = self.backend.defaults() + cmd_def = defaults.build_cmd_def() + + cx_pv = cmd_def.get('cx', (0, 1), P2=0) + pv_found = False + for _, instr in cx_pv.instructions: + cmd = instr.command + if isinstance(cmd, PersistentValue): + self.assertEqual(cmd.value, 1) + pv_found = True + self.assertTrue(pv_found) + + self.assertEqual(cmd_def.get_parameters('u1', 0), ('P1',)) + + u1_minus_pi = cmd_def.get('u1', 0, P1=1) + fc_cmd = u1_minus_pi.instructions[0][-1].command + self.assertEqual(fc_cmd.phase, np.pi) diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py index 9727613192..79b17d0d8b 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -12,15 +12,20 @@ import numpy as np from qiskit.test import QiskitTestCase -from qiskit.qobj import PulseQobjInstruction, PulseQobjExperimentConfig -from qiskit.qobj.converters import PulseQobjConverter, LoConfigConverter -from qiskit.pulse.commands import SamplePulse, FrameChange, PersistentValue, Snapshot, Acquire +from qiskit.qobj import (PulseQobjInstruction, PulseQobjExperimentConfig, PulseLibraryItem, + QobjMeasurementOption) +from qiskit.qobj.converters import (InstructionToQobjConverter, QobjToInstructionConverter, + LoConfigConverter) +from qiskit.qobj.converters.pulse_instruction import _is_math_expr_safe +from qiskit.pulse.commands import (SamplePulse, FrameChange, PersistentValue, Snapshot, Acquire, + Discriminator, Kernel) from qiskit.pulse.channels import (DeviceSpecification, Qubit, AcquireChannel, DriveChannel, - MeasureChannel, RegisterSlot, MemorySlot) + ControlChannel, MeasureChannel, RegisterSlot, MemorySlot,) +from qiskit.pulse.schedule import ParameterizedSchedule from qiskit.pulse import LoConfig -class TestInstructionConverter(QiskitTestCase): +class TestInstructionToQobjConverter(QiskitTestCase): """Pulse converter tests.""" def setUp(self): @@ -38,7 +43,7 @@ class TestInstructionConverter(QiskitTestCase): def test_drive_instruction(self): """Test converted qobj from PulseInstruction.""" - converter = PulseQobjConverter(PulseQobjInstruction, meas_level=2) + converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) command = SamplePulse(np.arange(0, 0.01), name='linear') instruction = command(self.device.q[0].drive) @@ -52,7 +57,7 @@ class TestInstructionConverter(QiskitTestCase): def test_frame_change(self): """Test converted qobj from FrameChangeInstruction.""" - converter = PulseQobjConverter(PulseQobjInstruction, meas_level=2) + converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) command = FrameChange(phase=0.1) instruction = command(self.device.q[0].drive) @@ -67,7 +72,7 @@ class TestInstructionConverter(QiskitTestCase): def test_persistent_value(self): """Test converted qobj from PersistentValueInstruction.""" - converter = PulseQobjConverter(PulseQobjInstruction, meas_level=2) + converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) command = PersistentValue(value=0.1j) instruction = command(self.device.q[0].drive) @@ -82,7 +87,7 @@ class TestInstructionConverter(QiskitTestCase): def test_acquire(self): """Test converted qobj from AcquireInstruction.""" - converter = PulseQobjConverter(PulseQobjInstruction, meas_level=2) + converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) command = Acquire(duration=10) instruction = command(self.device.q, self.device.mem, self.device.c) @@ -99,7 +104,7 @@ class TestInstructionConverter(QiskitTestCase): def test_snapshot(self): """Test converted qobj from SnapShot.""" - converter = PulseQobjConverter(PulseQobjInstruction, meas_level=2) + converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) instruction = Snapshot(name='label', snap_type='type') valid_qobj = PulseQobjInstruction( @@ -112,6 +117,138 @@ class TestInstructionConverter(QiskitTestCase): self.assertEqual(converter(0, instruction), valid_qobj) +class TestQobjToInstructionConverter(QiskitTestCase): + """Pulse converter tests.""" + + def setUp(self): + self.linear = SamplePulse(np.arange(0, 0.01), name='linear') + self.pulse_library = [PulseLibraryItem(name=self.linear.name, + samples=self.linear.samples.tolist())] + + self.converter = QobjToInstructionConverter(self.pulse_library, buffer=0) + + self.device = DeviceSpecification( + qubits=[ + Qubit(0, DriveChannel(0), MeasureChannel(0), AcquireChannel(0)) + ], + registers=[ + RegisterSlot(0) + ], + mem_slots=[ + MemorySlot(0) + ] + ) + + def test_drive_instruction(self): + """Test converted qobj from PulseInstruction.""" + cmd = self.linear + instruction = cmd(DriveChannel(0)) << 10 + + qobj = PulseQobjInstruction(name='linear', ch='d0', t0=10) + converted_instruction = self.converter(qobj) + + self.assertEqual(converted_instruction.timeslots, instruction.timeslots) + self.assertEqual(converted_instruction.instructions[0][-1].command, cmd) + + def test_frame_change(self): + """Test converted qobj from FrameChangeInstruction.""" + cmd = FrameChange(phase=0.1) + instruction = cmd(MeasureChannel(0)) + + qobj = PulseQobjInstruction(name='fc', ch='m0', t0=0, phase=0.1) + converted_instruction = self.converter(qobj) + + self.assertEqual(converted_instruction.timeslots, instruction.timeslots) + self.assertEqual(converted_instruction.instructions[0][-1].command, cmd) + + def test_persistent_value(self): + """Test converted qobj from PersistentValueInstruction.""" + cmd = PersistentValue(value=0.1j) + instruction = cmd(ControlChannel(1)) + + qobj = PulseQobjInstruction(name='pv', ch='u1', t0=0, val=0.1j) + converted_instruction = self.converter(qobj) + + self.assertEqual(converted_instruction.timeslots, instruction.timeslots) + self.assertEqual(converted_instruction.instructions[0][-1].command, cmd) + + def test_acquire(self): + """Test converted qobj from AcquireInstruction.""" + cmd = Acquire(10, Discriminator(name='test_disc', params={'test_params': 1.0}), + Kernel(name='test_kern', params={'test_params': 'test'})) + instruction = cmd(self.device.q, self.device.mem, self.device.c) + + qobj = PulseQobjInstruction(name='acquire', t0=0, duration=10, qubits=[0], + memory_slot=[0], register_slot=[0], + kernels=[QobjMeasurementOption( + name='test_kern', params={'test_params': 'test'})], + discriminators=[QobjMeasurementOption( + name='test_disc', params={'test_params': 1.0})]) + converted_instruction = self.converter(qobj) + + self.assertEqual(converted_instruction.timeslots, instruction.timeslots) + self.assertEqual(converted_instruction.instructions[0][-1].command, cmd) + + def test_snapshot(self): + """Test converted qobj from SnapShot.""" + cmd = Snapshot(name='label', snap_type='type') + instruction = cmd << 10 + + qobj = PulseQobjInstruction(name='snapshot', t0=10, label='label', type='type') + converted_instruction = self.converter(qobj) + + self.assertEqual(converted_instruction.timeslots, instruction.timeslots) + self.assertEqual(converted_instruction.instructions[0][-1], cmd) + + def test_parameterized_frame_change(self): + """Test converted qobj from FrameChangeInstruction.""" + cmd = FrameChange(phase=4.) + instruction = cmd(MeasureChannel(0)) << 10 + + qobj = PulseQobjInstruction(name='fc', ch='m0', t0=10, phase='P1**2') + converted_instruction = self.converter(qobj) + + self.assertIsInstance(converted_instruction, ParameterizedSchedule) + + evaluated_instruction = converted_instruction.bind_parameters(2.) + + self.assertEqual(evaluated_instruction.timeslots, instruction.timeslots) + self.assertEqual(evaluated_instruction.instructions[0][-1].command, cmd) + + def test_parameterized_persistent_value(self): + """Test converted qobj from PersistentValueInstruction.""" + cmd = PersistentValue(value=0.5+0.j) + instruction = cmd(ControlChannel(1)) << 10 + + qobj = PulseQobjInstruction(name='pv', ch='u1', t0=10, val='P1*cos(np.pi*P2)') + converted_instruction = self.converter(qobj) + + self.assertIsInstance(converted_instruction, ParameterizedSchedule) + + evaluated_instruction = converted_instruction.bind_parameters(P1=0.5, P2=0.) + + self.assertEqual(evaluated_instruction.timeslots, instruction.timeslots) + self.assertEqual(evaluated_instruction.instructions[0][-1].command, cmd) + + def test_expression_sanitizer(self): + """Test math expression sanitization.""" + + self.assertFalse(_is_math_expr_safe('INSERT INTO students VALUES (?,?)')) + self.assertFalse(_is_math_expr_safe('import math')) + self.assertFalse(_is_math_expr_safe('complex')) + self.assertFalse(_is_math_expr_safe('2***2')) + self.assertFalse(_is_math_expr_safe('avdfd*3')) + self.assertFalse(_is_math_expr_safe('Cos(1+2)')) + self.assertFalse(_is_math_expr_safe('hello_world')) + self.assertFalse(_is_math_expr_safe('1_2')) + self.assertFalse(_is_math_expr_safe('2+-2')) + + self.assertTrue(_is_math_expr_safe('1+1*2*3.2+8*cos(1)**2')) + self.assertTrue(_is_math_expr_safe('pi*2')) + self.assertTrue(_is_math_expr_safe('-P1*cos(P2)')) + self.assertTrue(_is_math_expr_safe('-P1*P2*P3')) + + class TestLoConverter(QiskitTestCase): """LO converter tests.""" diff --git a/test/python/qobj/test_qobj.py b/test/python/qobj/test_qobj.py index 6b7b10c320..0da4bd8476 100644 --- a/test/python/qobj/test_qobj.py +++ b/test/python/qobj/test_qobj.py @@ -27,7 +27,7 @@ from qiskit.providers.basicaer import basicaerjob from qiskit.qobj import (QasmQobj, PulseQobj, QobjHeader, PulseQobjInstruction, PulseQobjExperiment, PulseQobjConfig, QobjMeasurementOption, - QobjPulseLibrary, QasmQobjInstruction, + PulseLibraryItem, QasmQobjInstruction, QasmQobjExperiment, QasmQobjConfig) from qiskit.qobj import validate_qobj_against_schema from qiskit.validation.jsonschema.exceptions import SchemaValidationError @@ -146,7 +146,7 @@ class TestPulseQobj(QiskitTestCase): memory_slot_size=8192, meas_return='avg', pulse_library=[ - QobjPulseLibrary(name='pulse0', + PulseLibraryItem(name='pulse0', samples=[0.0 + 0.0j, 0.5 + 0.0j, 0.0 + 0.0j]) @@ -221,7 +221,7 @@ class TestPulseQobj(QiskitTestCase): memory_slot_size=8192, meas_return='avg', pulse_library=[ - QobjPulseLibrary(name='pulse0', samples=[0.1 + 0.0j]) + PulseLibraryItem(name='pulse0', samples=[0.1 + 0.0j]) ], qubit_lo_freq=[4.9], meas_lo_freq=[6.9], rep_time=1000), @@ -233,8 +233,8 @@ class TestPulseQobj(QiskitTestCase): 'meas_lo_freq': [6.9], 'rep_time': 1000}, ), - QobjPulseLibrary: ( - QobjPulseLibrary(name='pulse0', samples=[0.1 + 0.0j]), + PulseLibraryItem: ( + PulseLibraryItem(name='pulse0', samples=[0.1 + 0.0j]), {'name': 'pulse0', 'samples': [[0.1, 0.0]]} ), PulseQobjExperiment: (