Skip to content

Commit

Permalink
Eliminate use of Schedule in the builder context. Schedule is implici…
Browse files Browse the repository at this point in the history
…tly converted into ScheduleBlock with AreaBarrier instructions.
  • Loading branch information
nkanazawa1989 committed Oct 19, 2022
1 parent b7e6329 commit 805a5b2
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 83 deletions.
205 changes: 142 additions & 63 deletions qiskit/pulse/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,7 @@ def __init__(
if isinstance(block, ScheduleBlock):
root_block = block
elif isinstance(block, Schedule):
root_block = ScheduleBlock()
root_block.append(instructions.Call(subroutine=block))
root_block = self._naive_typecast_schedule(block)
else:
raise exceptions.PulseError(
f"Input `block` type {block.__class__.__name__} is "
Expand Down Expand Up @@ -706,7 +705,7 @@ def _compile_lazy_circuit(self):
lazy_circuit = self._lazy_circuit
# reset lazy circuit
self._lazy_circuit = self._new_circuit()
self.call_subroutine(subroutine=self._compile_circuit(lazy_circuit))
self.call_subroutine(self._compile_circuit(lazy_circuit))

def _compile_circuit(self, circ) -> Schedule:
"""Take a QuantumCircuit and output the pulse schedule associated with the circuit."""
Expand All @@ -731,27 +730,74 @@ def append_instruction(self, instruction: instructions.Instruction):
"""
self._context_stack[-1].append(instruction)

def append_reference(self, name: str, *extra_keys: str):
"""Add external program as a :class:`~qiskit.pulse.instructions.Reference` instruction.
Args:
name: Name of subroutine.
extra_keys: Assistance keys to uniquely specify the subroutine.
"""
inst = instructions.Reference(name, *extra_keys)
self.append_instruction(inst)

@_compile_lazy_circuit_before
def append_block(self, context_block: ScheduleBlock):
"""Add a :class:`ScheduleBlock` to the builder's context schedule.
Args:
context_block: ScheduleBlock to append to the current context block.
Raises:
PulseError: When non ScheduleBlock object is appended.
"""
if not isinstance(context_block, ScheduleBlock):
raise exceptions.PulseError(
f"'{context_block.__class__.__name__}' is not valid data format in the builder. "
"Only 'ScheduleBlock' can be appended to the builder context."
)

# ignore empty context
if len(context_block) > 0:
self._context_stack[-1].append(context_block)

def append_reference(self, name: str, *extra_keys: str):
"""Add external program as a :class:`~qiskit.pulse.instructions.Reference` instruction.
@functools.singledispatchmethod
def inject_subroutine(
self,
subroutine: Union[Schedule, ScheduleBlock],
):
"""Append a :class:`ScheduleBlock` to the builder's context schedule.
This operationd doesn't create reference. Subrotuine is directly
injected into current context schedule.
Args:
name: Name of subroutine.
extra_keys: Assistance keys to uniquely specify the subroutine.
subroutine: ScheduleBlock to append to the current context block.
Raises:
PulseError: When subroutine is not Schedule nor ScheduleBlock.
"""
inst = instructions.Reference(name, *extra_keys)
self.append_instruction(inst)
raise exceptions.PulseError(
f"Subroutine type {subroutine.__class__.__name__} is "
"not valid data format. Inject Schedule or ScheduleBlock."
)

@inject_subroutine.register
def _(self, block: ScheduleBlock):
self._compile_lazy_circuit()

if len(block) == 0:
return
self._context_stack[-1].append(block)

@inject_subroutine.register
def _(self, schedule: Schedule):
self._compile_lazy_circuit()

if len(schedule) == 0:
return
self._context_stack[-1].append(self._naive_typecast_schedule(schedule))

@functools.singledispatchmethod
def call_subroutine(
self,
subroutine: Union[circuit.QuantumCircuit, Schedule, ScheduleBlock],
Expand All @@ -778,34 +824,37 @@ def call_subroutine(
Raises:
PulseError:
- When specified parameter is not contained in the subroutine
- When input subroutine is not valid data format.
"""
if isinstance(subroutine, circuit.QuantumCircuit):
self._compile_lazy_circuit()
subroutine = self._compile_circuit(subroutine)

if not isinstance(subroutine, (Schedule, ScheduleBlock)):
raise exceptions.PulseError(
f"Subroutine type {subroutine.__class__.__name__} is "
"not valid data format. Call QuantumCircuit, "
"Schedule, or ScheduleBlock."
)
raise exceptions.PulseError(
f"Subroutine type {subroutine.__class__.__name__} is "
"not valid data format. Call QuantumCircuit, "
"Schedule, or ScheduleBlock."
)

if len(subroutine) == 0:
@call_subroutine.register
def _(
self,
target_block: ScheduleBlock,
name: Optional[str] = None,
value_dict: Optional[Dict[ParameterExpression, ParameterValueType]] = None,
**kw_params: ParameterValueType,
):
if len(target_block) == 0:
return

# Create local parameter assignment
local_assignment = dict()
for param_name, value in kw_params.items():
params = subroutine.get_parameters(param_name)
params = target_block.get_parameters(param_name)
if not params:
raise exceptions.PulseError(
f"Parameter {param_name} is not defined in the target subroutine. "
f'{", ".join(map(str, subroutine.parameters))} can be specified.'
f'{", ".join(map(str, target_block.parameters))} can be specified.'
)
for param in params:
local_assignment[param] = value

if value_dict:
if local_assignment.keys() & value_dict.keys():
warnings.warn(
Expand All @@ -816,22 +865,54 @@ def call_subroutine(
)
local_assignment.update(value_dict)

if isinstance(subroutine, ScheduleBlock):
# If subroutine is schedule block, use reference mechanism.
if local_assignment:
subroutine = subroutine.assign_parameters(local_assignment, inplace=False)
if name is None:
# Add unique string, not to accidentally override existing reference entry.
keys = (subroutine.name, uuid.uuid4().hex)
else:
keys = (name,)
self.append_reference(*keys)
self.get_context().assign_references({keys: subroutine}, inplace=True)
if local_assignment:
target_block = target_block.assign_parameters(local_assignment, inplace=False)

if name is None:
# Add unique string, not to accidentally override existing reference entry.
keys = (target_block.name, uuid.uuid4().hex)
else:
# If subroutine is schedule, use Call instruction.
name = name or subroutine.name
call_instruction = instructions.Call(subroutine, local_assignment, name)
self.append_instruction(call_instruction)
keys = (name,)

self.append_reference(*keys)
self.get_context().assign_references({keys: target_block}, inplace=True)

@call_subroutine.register
def _(
self,
target_schedule: Schedule,
name: Optional[str] = None,
value_dict: Optional[Dict[ParameterExpression, ParameterValueType]] = None,
**kw_params: ParameterValueType,
):
if len(target_schedule) == 0:
return

self.call_subroutine(
self._naive_typecast_schedule(target_schedule),
name=name,
value_dict=value_dict,
**kw_params,
)

@call_subroutine.register
def _(
self,
target_circuit: circuit.QuantumCircuit,
name: Optional[str] = None,
value_dict: Optional[Dict[ParameterExpression, ParameterValueType]] = None,
**kw_params: ParameterValueType,
):
if len(target_circuit) == 0:
return

self._compile_lazy_circuit()
self.call_subroutine(
self._compile_circuit(target_circuit),
name=name,
value_dict=value_dict,
**kw_params,
)

@_requires_backend
def call_gate(self, gate: circuit.Gate, qubits: Tuple[int, ...], lazy: bool = True):
Expand Down Expand Up @@ -870,6 +951,21 @@ def _call_gate(self, gate, qargs):

self._lazy_circuit.append(gate, qargs=qargs)

@staticmethod
def _naive_typecast_schedule(schedule: Schedule):
# Naively convert into ScheduleBlock
from qiskit.pulse.transforms import inline_subroutines, flatten, pad

preprocessed_schedule = inline_subroutines(flatten(schedule))
pad(preprocessed_schedule, inplace=True, pad_with=instructions.AreaBarrier)

# default to left alignment, namely ASAP scheduling
target_block = ScheduleBlock(name=schedule.name)
for _, inst in preprocessed_schedule.instructions:
target_block.append(inst, inplace=True)

return target_block


def build(
backend=None,
Expand Down Expand Up @@ -973,21 +1069,9 @@ def append_schedule(schedule: Union[Schedule, ScheduleBlock]):
"""Call a schedule by appending to the active builder's context block.
Args:
schedule: Schedule to append.
Raises:
PulseError: When input `schedule` is invalid data format.
schedule: Schedule or ScheduleBlock to append.
"""
if isinstance(schedule, Schedule):
_active_builder().append_instruction(instructions.Call(subroutine=schedule))
elif isinstance(schedule, ScheduleBlock):
_active_builder().append_block(schedule)
else:
raise exceptions.PulseError(
f"Input program {schedule.__class__.__name__} is not "
"acceptable program format. Input `Schedule` or "
"`ScheduleBlock`."
)
_active_builder().inject_subroutine(schedule)


def append_instruction(instruction: instructions.Instruction):
Expand Down Expand Up @@ -1986,16 +2070,8 @@ def call(
the parameters having the same name are all updated together.
If you want to avoid name collision, use ``value_dict`` with :class:`~.Parameter`
objects instead.
Raises:
exceptions.PulseError: If the input ``target`` type is not supported.
"""
if not isinstance(target, (circuit.QuantumCircuit, Schedule, ScheduleBlock)):
raise exceptions.PulseError(f"'{target.__class__.__name__}' is not a valid target object.")

_active_builder().call_subroutine(
subroutine=target, name=name, value_dict=value_dict, **kw_params
)
_active_builder().call_subroutine(target, name, value_dict, **kw_params)


def reference(name: str, *extra_keys: str):
Expand Down Expand Up @@ -2237,7 +2313,10 @@ def measure(

# note this is not a subroutine.
# just a macro to automate combination of stimulus and acquisition.
_active_builder().call_subroutine(measure_sched)
# prepare unique reference name based on qubit and memory slot index.
qubits_repr = "&".join(map(str, qubits))
mslots_repr = "&".join(map(lambda r: str(r.index), registers))
_active_builder().call_subroutine(measure_sched, name=f"measure_{qubits_repr}..{mslots_repr}")

if len(qubits) == 1:
return registers[0]
Expand Down Expand Up @@ -2283,7 +2362,7 @@ def measure_all() -> List[chans.MemorySlot]:

# note this is not a subroutine.
# just a macro to automate combination of stimulus and acquisition.
_active_builder().call_subroutine(measure_sched)
_active_builder().call_subroutine(measure_sched, name="measure_all")

return registers

Expand Down
2 changes: 1 addition & 1 deletion qiskit/pulse/instructions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"""
from .acquire import Acquire
from .delay import Delay
from .directives import Directive, RelativeBarrier
from .directives import Directive, RelativeBarrier, AreaBarrier
from .call import Call
from .instruction import Instruction
from .frequency import SetFrequency, ShiftFrequency
Expand Down
77 changes: 77 additions & 0 deletions qiskit/pulse/instructions/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,80 @@ def channels(self) -> Tuple[chans.Channel]:
def __eq__(self, other):
"""Verify two barriers are equivalent."""
return isinstance(other, type(self)) and set(self.channels) == set(other.channels)


class AreaBarrier(Directive):
"""Pulse ``AreaBarrier`` directive.
This instruction is intended to be used internally within the pulse builder,
to naively convert :class:`.Schedule` into :class:`.ScheduleBlock`.
Becasue :class:`.ScheduleBlock` cannot take absolute instruction interval,
this instruction helps the block represetation with finding instruction starting time.
Example:
This schedule plays constant pulse at t0 = 120.
.. code-block:: python
schedule = Schedule()
schedule.insert(120, Play(Constant(10, 0.1), DriveChannel(0)))
This schedule block is expected to be identical to above at a time of execution.
.. code-block:: python
block = ScheduleBlock()
block.append(AreaBarrier(120, DriveChannel(0)))
block.append(Play(Constant(10, 0.1), DriveChannel(0)))
Such conversion may be done by
.. code-block:: python
from qiskit.pulse.transforms import block_to_schedule, remove_directives
schedule = remove_directives(block_to_schedule(block))
.. note::
The AreaBarrier instruction behaves almost identically
to :class:`~qiskit.pulse.instructions.Delay` instruction.
However, the AreaBarrier is just a compiler directive and must be removed before execution.
This may be done by :func:`~qiskit.pulse.transforms.remove_directives` transform.
Once these directives are removed, occupied timeslots are released and
user can insert another instruction without timing overlap.
"""

def __init__(
self,
duration: int,
channel: chans.Channel,
name: Optional[str] = None,
):
"""Create an area barrier directive.
Args:
duration: Length of time of the occupation in terms of dt.
channel: The channel that will be the occupied.
name: Name of the area barrier for display purposes.
"""
super().__init__(operands=(duration, channel), name=name)

@property
def channel(self) -> chans.Channel:
"""Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is
scheduled on.
"""
return self.operands[1]

@property
def channels(self) -> Tuple[chans.Channel]:
"""Returns the channels that this schedule uses."""
return (self.channel,)

@property
def duration(self) -> int:
"""Duration of this instruction."""
return self.operands[0]
Loading

0 comments on commit 805a5b2

Please sign in to comment.