# Copyright 2019 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=too-many-return-statements,too-many-branches,too-many-instance-attributes
"""
Blackbird Program class
=======================
**Module name:** `blackbird.program`
.. currentmodule:: blackbird.program
This module contains a Python class representing a Blackbird
program using standard Python data types.
The functions :func:`~.load`, and :func:`~.loads` will read Blackbird scripts
and return an instance of the :class:`BlackbirdProgram` class.
Summary
-------
.. autosummary::
numpy_to_blackbird
BlackbirdProgram
Code details
~~~~~~~~~~~~
"""
import copy
from typing import Iterable
import numpy as np
import sympy as sym
[docs]def numpy_to_blackbird(A, var_name):
"""Converts a numpy array to a Blackbird script array type.
Args:
A (array): 2-dimensional NumPy array
var_name (str): the array variable name
Returns:
list[str]: list containing each line representing the
Blackbird array variable declaration
"""
if np.issubdtype(A.dtype, np.complexfloating):
# complex array
script = ["complex array {}[{}, {}] =".format(var_name, *A.shape)]
for row in A:
row_str = " " + ", ".join(
["{0}{1}{2}j".format(n.real, "+-"[int(n.imag < 0)], abs(n.imag)) for n in row]
)
script.append(row_str)
elif np.issubdtype(A.dtype, np.integer):
# integer array
script = ["int array {}[{}, {}] =".format(var_name, *A.shape)]
for row in A:
row_str = " " + ", ".join(["{}".format(int(n)) for n in row])
script.append(row_str)
elif np.issubdtype(A.dtype, np.floating):
# real array
script = ["float array {}[{}, {}] =".format(var_name, *A.shape)]
for row in A:
row_str = " " + ", ".join(["{}".format(n) for n in row])
script.append(row_str)
else:
# unknown array type
raise ValueError("Array {} is of unsupported type {}".format(A, A.dtype))
script.append("")
return script
[docs]class BlackbirdProgram:
"""Python representation of a Blackbird program."""
[docs] def __init__(self, name="blackbird_program", version="1.0"):
self._var = {}
self._forvar = {}
self._modes = set()
# the following attributes fully describe a Blackbird program
self._name = name
self._version = version
self._target = {"name": None, "options": dict()}
self._type = {"name": None, "options": dict()}
self._operations = []
self._parameters = []
@property
def name(self):
"""Name of the Blackbird program
Returns:
str: name
"""
return self._name
@property
def version(self):
"""Version of the Blackbird parser the program targets
Returns:
str: version number
"""
return self._version
@property
def modes(self):
"""A set of non-negative integers specifying the mode numbers the program manipulates.
Returns:
set[int]: mode numbers
"""
return self._modes
@property
def target(self):
"""Contains information regarding the target device of the quantum
program (i.e., the target device the Blackbird script is compiled for).
Important keys include:
* ``'name'`` (Union[str, None]): the name of the device the Blackbird script requests to be
run on. If no target is requested, the returned value will be ``None``.
* ``'options'`` (dict): a dictionary of keyword arguments for the target device
Returns:
dict[str->[str, dict]]: target information
"""
return self._target
@property
def programtype(self):
"""Information regarding the type of program that is to be run on the device.
Important keys include:
* ``'name'`` (Union[str, None]): the name of the type of program that is to be run on the
device (e.g. 'TDM'). If no type is requested, the returned value will be ``None``.
* ``'options'`` (dict): a dictionary of keyword arguments for the type (e.g. 'copies')
Returns:
dict[str->[str, dict]]: type information
"""
return self._type
@property
def operations(self):
"""List of operations to apply to the device, in temporal order.
Each operation is contained as a dictionary, with the following keys:
* ``'op'`` (str): the name of the operation
* ``'args'`` (list): a list of positional arguments for the operation
* ``'kwargs'`` (dict): a dictionary of keyword arguments for the operation
* ``'modes'`` (list[int]): modes the operation applies to
Note that, depending on the operation, both ``'args'`` and ``'kwargs'``
might be empty.
Returns:
list[dict]: operation information
"""
return self._operations
@property
def parameters(self):
"""List of free parameters the Blackbird script depends on.
Returns:
List[str]: list of free parameter names
"""
return set([str(i) for i in self._parameters])
@property
def variables(self):
"""List of variables in the Blackbird program.
Returns:
dict[str, float]: dictionary of variables
"""
return self._var
[docs] def is_template(self):
"""Returns ``True`` if there is at least one free parameter.
Returns:
bool: True if a template
"""
return bool(self.parameters)
[docs] def __call__(self, **kwargs):
"""Create a new Blackbird program, with all free parameters
initialized to their passed values.
Returns:
Program:
"""
if not self.parameters:
raise ValueError("Program is not a template!")
prog = copy.deepcopy(self)
prog._parameters = [] # pylint: disable=protected-access
# extract the values in any kwarg Iterables so that these correspond to
# the single valued parsed parameters `parametername_i_j`
new_kwargs = copy.deepcopy(kwargs)
for k, v in kwargs.items():
if isinstance(v, Iterable):
if np.ndim(v) != 2:
raise ValueError("Invalid dim for free parameter provided. Must have dim 2.")
added_kwargs = {
k + "_{}_{}".format(i, j): val
for i, row in enumerate(v)
for j, val in enumerate(row)
}
new_kwargs.update(added_kwargs)
del new_kwargs[k]
kwargs = new_kwargs
# set values for args and kwargs in operations
for op in prog._operations: # pylint: disable=protected-access
if 'args' not in op:
continue
for idx, a in enumerate(op['args']):
if isinstance(a, sym.Expr):
par = list(a.free_symbols)
func = sym.lambdify(par, a)
try:
vals = {str(p): kwargs[str(p)] for p in par}
except KeyError:
raise ValueError("Invalid value for free parameter provided")
op['args'][idx] = func(**vals)
for k, v in op['kwargs'].items():
if isinstance(v, sym.Expr):
par = list(v.free_symbols)
func = sym.lambdify(par, v)
try:
vals = {str(p): kwargs[str(p)] for p in par}
except KeyError:
raise ValueError("Invalid value for free parameter provided")
op['kwargs'][k] = func(**vals)
# set values for variables and arrays
for k, v in prog._var.items(): # pylint: disable=protected-access
# it can either be an independent parameter for a variable
if isinstance(v, sym.Expr):
par = list(v.free_symbols)
func = sym.lambdify(par, v)
try:
vals = {str(p): kwargs[str(p)] for p in par}
except KeyError:
raise ValueError("Invalid value for free parameter provided")
prog._var[k] = func(**vals)
# or encapsulated in an array
elif isinstance(v, np.ndarray):
# look through the array and, if there are any parameters,
# replace them with their corresponding values from kwargs
populated_array = copy.deepcopy(v)
for i, j in np.ndindex(v.shape):
if isinstance(v[i][j], sym.Expr):
par = list(v[i][j].free_symbols)
func = sym.lambdify(par, v[i][j])
try:
vals = {str(p): kwargs[str(p)] for p in par}
except KeyError:
raise ValueError("Invalid value for free parameter provided")
populated_array[i][j] = func(**vals)
prog._var[k] = populated_array
return prog
[docs] def __len__(self):
"""The length of the quantum program (i.e., the number of operations applied).
Returns:
int: program length
"""
return len(self._operations)
[docs] def serialize(self):
"""Serializes the blackbird program, returning a valid Blackbird script
as a string.
Returns:
str: the blackbird script representing the BlackbirdProgram object
"""
# pylint: disable=too-many-statements
# top level metadata
var_count = 0
array_insert = 3
script = ["name {}".format(self.name), "version {}".format(self.version)]
# add target and type to the script
for name, data in [("target", self.target), ("type", self.programtype)]:
if data["name"] is not None:
array_insert += 1
options = ""
if data["options"]:
# if the target has options, compile them into
# the expected syntax
option_strings = []
for k, v in data["options"].items():
if not isinstance(v, str):
option_strings.append("{}={}".format(k, v))
else:
option_strings.append('{}="{}"'.format(k, v))
options = " ({})".format(", ".join(option_strings))
# add metadata
script.append("{} {}{}".format(name, data["name"], options))
# line break
script.append("")
# add variables to the script
if self.programtype["name"] == "tdm":
from .listener import NUMPY_TYPES # pylint:disable=import-outside-toplevel
inv_type_map = {np.dtype(v).kind: k for k, v in NUMPY_TYPES.items()}
for k, v in self._var.items():
var_type = inv_type_map[np.array(v).dtype.kind]
array_string = ""
if isinstance(v, Iterable):
for row in v:
array_string += "\n " + "".join("{}, ".format(i) for i in row)[:-2]
script.append("{} array {} ={}".format(var_type, k, array_string))
else:
script.append("{} array {} =\n{}".format(var_type, k, v))
# line break
script.append("")
# loop through each quantum operation
for op in self.operations:
if len(op["modes"]) == 1:
modes = op["modes"][0]
else:
modes = op["modes"]
# check if the operation has any arguments
if "args" in op:
args = []
kwargs = []
# loop through position arguments
for v in op["args"]:
# for each operation argument, format it
# correctly depending on its type
if isinstance(v, np.ndarray):
# create an array variable
var_name = "A{}".format(var_count)
args.append(var_name)
var_count += 1
# add array declaration to script after the metadata block
bb_array = numpy_to_blackbird(v, var_name)
for idx, line in enumerate(bb_array):
script.insert(array_insert + idx, line)
array_insert += len(bb_array)
elif isinstance(v, str):
# argument is a string type; if a p-type parameter (e.g. p0),
# then simply add it as is
if self.programtype["name"] == "tdm" and v[0] == "p" and v[1:].isdigit():
args.append(v)
else:
args.append('"{}"'.format(v))
elif isinstance(v, complex):
# argument is a complex type
args.append("{}{}{}j".format(v.real, "+-"[int(v.imag < 0)], np.abs(v.imag)))
elif isinstance(v, sym.Expr):
# argument contains free parameters
res = str(v)
for p in v.free_symbols:
res = res.replace(str(p), "{"+str(p)+"}")
args.append(res)
else:
# anything that doesn't need to be dealt with as a special case,
# i.e., booleans, ints, floats.
args.append("{}".format(v))
# loop through keyword argument
for k, v in op["kwargs"].items():
# for each operation argument, format it
# correctly depending on its type
if isinstance(v, np.ndarray):
# create an array variable
var_name = "A{}".format(var_count)
kwargs.append("{}={}".format(k, var_name))
var_count += 1
# add array declaration to script
bb_array = numpy_to_blackbird(v, var_name)
for idx, line in enumerate(bb_array):
script.insert(array_insert + idx, line)
array_insert += len(bb_array)
elif isinstance(v, str):
# kwarg is a string type; if a p-type parameter (e.g. p0),
# then simply add it as is
if self.programtype["name"] == "tdm" and v[0] == "p" and v[1:].isdigit():
kwargs.append("{}={}".format(k, v))
else:
kwargs.append('{}="{}"'.format(k, v))
elif isinstance(v, complex):
kwargs.append(
"{}={}{}{}j".format(k, v.real, "+-"[int(v.imag < 0)], np.abs(v.imag))
)
else:
kwargs.append("{}={}".format(k, v))
if args and kwargs:
arguments = "({}, {})".format(", ".join(args), ", ".join(kwargs))
elif not kwargs:
arguments = "({})".format(", ".join(args))
elif not args:
arguments = "({})".format(", ".join(kwargs))
script.append("{}{} | {}".format(op["op"], arguments, modes))
else:
# operation has no arguments
script.append("{} | {}".format(op["op"], modes))
if script[-1] != "":
# add a newline
script.append("")
return "\n".join(script)