Source code for strawberryfields.program
# Copyright 2019-2022 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.
"""
This module implements the :class:`.Program` class which acts as a representation for quantum circuits.
Quantum circuit representation
------------------------------
The :class:`.Command` instances in the circuit form a
`strict partially ordered set <http://en.wikipedia.org/wiki/Partially_ordered_set#Strict_and_non-strict_partial_orders>`_
in the sense that the order in which the operations have to be executed is usually not completely fixed.
For example, operations acting on different subsystems always commute with each other.
We denote :math:`a < b` if :math:`a` has to be executed before :math:`b`.
Each strict partial order corresponds to a
`directed acyclic graph <http://en.wikipedia.org/wiki/Directed_acyclic_graph>`_ (DAG),
and the transitive closure of any DAG is a strict partial order.
Three different (but equivalent) representations of the circuit are used.
* Initially, the circuit is represented as a Command queue (list), listing the Commands in
the temporal order they are applied.
* The second representation, grid, essentially mimics a quantum circuit diagram.
It is a mapping from subsystem indices to lists of Commands touching that subsystem,
where each list is temporally ordered.
* Finally, the quantum circuit can be represented using a DAG by making each Command a node,
and drawing an edge from each Command to all its immediate followers along each wire it touches.
It can be converted back into a command queue by popping a maximal element until the graph
is empty, that is, consuming it in a topological order.
Note that a topological order is not always unique, there may be several equivalent topological orders.
.. currentmodule:: strawberryfields.program_utils
The three representations can be converted to each other
using the functions :func:`list_to_grid`, :func:`grid_to_DAG` and :func:`DAG_to_list`.
.. currentmodule:: strawberryfields.program
"""
# pylint: disable=too-many-instance-attributes,attribute-defined-outside-init
import copy
import numbers
import warnings
import networkx as nx
import blackbird as bb
from blackbird.utils import match_template, TemplateError
import strawberryfields as sf
import strawberryfields.circuitdrawer as sfcd
from strawberryfields.compilers import Compiler, compiler_db
import strawberryfields.program_utils as pu
from .program_utils import (
Command,
RegRef,
CircuitError,
RegRefError,
program_equivalence,
remove_loss,
)
from .parameters import FreeParameter, ParameterError
# for automodapi, do not include the classes that should appear under the top-level strawberryfields namespace
__all__ = []
ALLOWED_RUN_OPTIONS = ["shots", "crop"]
[docs]class Program:
"""Represents a photonic quantum circuit.
The program class provides a context manager for:
* accessing the quantum register associated with the program, and
* appending :doc:`/introduction/ops` to the program.
Within the context, operations are appended to the program using the
Python-embedded Blackbird syntax
.. code-block:: python3
ops.GateName(arg1, arg2, ...) | (q[i], q[j], ...)
where ``ops.GateName`` is a valid quantum operation, and ``q`` is a list
of the programs quantum modes.
All operations are appended to the program in the order they are
listed within the context.
In addition, some 'meta-operations' (such as :func:`~.New` and :attr:`~.Del`)
are provided to modify the programs quantum register itself by adding
and deleting subsystems.
.. note::
Two programs can be run successively on the same engine if and only if
the number of registers at the end of the first program matches the
number of modes at the beginning of the second program.
This can be enforced by constructing the second program as an explicit
successor of the first, in which case the registers are directly copied over.
When a Program is run or it obtains a successor, it is locked and no more
operations can be appended to it.
**Example:**
.. code-block:: python3
import strawberryfields as sf
from strawberryfields import ops
# create a 3 mode quantum program
prog = sf.Program(3)
with prog.context as q:
ops.Sgate(0.54) | q[0]
ops.Sgate(0.54) | q[1]
ops.Sgate(0.54) | q[2]
ops.BSgate(0.43, 0.1) | (q[0], q[2])
ops.BSgate(0.43, 0.1) | (q[1], q[2])
ops.MeasureFock() | q
The currently active register references can be accessed using the :meth:`~Program.register` method.
.. note::
The ``Program`` equality operation (e.g., ``prog_1 == prog_2``) does not account for
logically equivalent programs where the order of operations or modes do not matter. It
simply checks that the operations and parameters are identically applied. For a more
thorough check, use the ``Program.equivalence()`` method instead.
Args:
num_subsystems (int, Program): Initial number of modes (subsystems) in the quantum register.
Alternatively, another Program instance from which to inherit the register state.
name (str): program name (optional)
"""
def __init__(self, num_subsystems, name=None):
#: str: program name
self.name = name
#: list[Command]: Commands constituting the quantum circuit in temporal order
self.circuit = []
#: bool: if True, no more Commands can be appended to the Program
self.locked = False
#: str, None: for compiled Programs, the short name of the target Compiler template, otherwise None
self._target = None
#: tuple, None: for compiled Programs, the device spec and the short
# name of Compiler that was used, otherwise None
self._compile_info = None
#: Program, None: for compiled Programs, this is the original, otherwise None
self.source = None
#: dict[str, Parameter]: free circuit parameters owned by this Program
self.free_params = {}
self.run_options = {}
"""dict[str, Any]: dictionary of default run options, to be passed to the engine upon
execution of the program. Note that if the ``run_options`` dictionary is passed
directly to :meth:`~.Engine.run`, it takes precedence over the run options specified
here.
"""
self.backend_options = {}
# create subsystem references
# Program keeps track of the state of the quantum register using a dictionary of :class:`RegRef` objects.
if isinstance(num_subsystems, numbers.Integral):
#: int: initial number of subsystems
self.init_num_subsystems = num_subsystems
#: dict[int, RegRef]: mapping from subsystem indices to corresponding RegRef objects
self.reg_refs = {}
#: set[int]: created subsystem indices that have not been used (operated on) yet
self.unused_indices = set()
self._add_subsystems(num_subsystems)
elif isinstance(num_subsystems, Program):
# it's the parent program
parent = num_subsystems
# copy the RegRef state from the parent program
parent.lock() # make sure the parent isn't accidentally updated by the user
self.init_num_subsystems = parent.num_subsystems
self.reg_refs = copy.deepcopy(parent.reg_refs) # independent copy of the RegRefs
self.unused_indices = copy.copy(parent.unused_indices)
else:
raise TypeError(
"First argument must be either the number of subsystems or the parent Program."
)
# save the initial regref state
#: dict[int, RegRef]: like reg_refs
self.init_reg_refs = copy.deepcopy(self.reg_refs)
#: set[int]: like unused_indices
self.init_unused_indices = copy.copy(self.unused_indices)
def __str__(self):
"""String representation."""
return self.__class__.__name__ + "({}, {}->{} subsystems, compiled for '{}')".format(
self.name, self.init_num_subsystems, self.num_subsystems, self.target
)
def __len__(self):
"""Program length.
Returns:
int: number of Commands in the program
"""
return len(self.circuit)
def __eq__(self, prog):
"""Equality operator for programs."""
# is the targets differ, the programs are not equal
if self.target != prog.target:
return False
# if the registers differ, the programs are not equal
if self.register != prog.register:
return False
# NOTE: If two programs have been compiled by different compilers (or only one has been
# compiled), they are still considered to be equal, even if their targets differ. Similarly,
# two programs with different names can still be considered equal.
for self_cmd, prog_cmd in zip(self.circuit, prog.circuit):
names_eq = self_cmd.op.__class__ == prog_cmd.op.__class__
param_eq = all(p1 == p2 for p1, p2 in zip(self_cmd.op.p, prog_cmd.op.p))
modes_eq = all(m1 == m2 for m1, m2 in zip(self_cmd.reg, prog_cmd.reg))
if not all((names_eq, param_eq, modes_eq)):
return False
return True
[docs] def equivalence(self, prog, **kwargs):
"""Checks if two programs are equivalent.
This function converts the program lists into directed acyclic graphs,
and runs the NetworkX ``is_isomorphic`` graph function in order
to determine if the two programs are equivalent.
.. note::
This method is a convenience method, wrapping the :func:`.program_equivalence`
function in the program utils module.
Args:
prog (strawberryfields.program.Program): quantum program to check equivalence with
Keyword args:
compare_params (bool): Set to ``False`` to turn of comparing program parameters;
equivalency will only take into account the operation order.
atol (float): the absolute tolerance parameter for checking quantum operation
parameter equality
rtol (float): the relative tolerance parameter for checking quantum operation
parameter equality
Returns:
bool: returns ``True`` if two quantum programs are equivalent
"""
return program_equivalence(self, prog, **kwargs)
[docs] def print(self, print_fn=print):
"""Print the program contents using Blackbird syntax.
**Example:**
.. code-block:: python
# create a 3 mode quantum program
prog = sf.Program(3)
with prog.context as q:
ops.Sgate(0.54) | q[0]
ops.Sgate(0.54) | q[1]
ops.Sgate(0.54) | q[2]
ops.BSgate(0.43, 0.1) | (q[0], q[2])
ops.BSgate(0.43, 0.1) | (q[1], q[2])
ops.MeasureFock() | q
>>> prog.print()
Sgate(0.54, 0) | (q[0])
Sgate(0.54, 0) | (q[1])
Sgate(0.54, 0) | (q[2])
BSgate(0.43, 0.1) | (q[0], q[2])
BSgate(0.43, 0.1) | (q[1], q[2])
MeasureFock | (q[0], q[1], q[2])
Args:
print_fn (function): optional custom function to use for string printing
"""
for k in self.circuit:
print_fn(k)
@property
def context(self):
"""Syntactic sugar for defining a Program using the :code:`with` statement.
The Program object itself acts as the context manager.
"""
return self
def __enter__(self):
"""Enter the context for this program.
Returns:
tuple[RegRef]: subsystem references
"""
if pu.Program_current_context is None:
pu.Program_current_context = self
else:
raise RuntimeError("Only one Program context can be active at a time.")
return self.register
def __exit__(self, ex_type, ex_value, ex_tb):
"""Exit the quantum circuit context."""
pu.Program_current_context = None
# =================================================
# RegRef accounting
# =================================================
@property
def register(self):
"""Return a tuple of all the currently valid quantum modes.
Returns:
tuple[RegRef]: valid subsystem references
"""
return tuple(r for r in self.reg_refs.values() if r.active)
@property
def num_subsystems(self):
"""Return the current number of valid quantum modes.
Returns:
int: number of currently valid register subsystems
"""
return len(self.register)
def _clear_regrefs(self):
"""Clear any measurement values stored in the RegRefs.
Called by :class:`~.engine.Engine` when resetting the backend.
"""
for r in self.reg_refs.values():
r.val = None
def _add_subsystems(self, n):
"""Create new subsystem references, add them to the reg_ref dictionary.
To avoid discrepancies with the backend this method must not be called directly,
but rather indirectly by using :func:`~strawberryfields.ops.New`
in a Program context.
.. note:: This is the only place where :class:`RegRef` instances are constructed.
Args:
n (int): number of subsystems to add
Returns:
tuple[RegRef]: tuple of the newly added subsystem references
"""
if self.locked:
raise CircuitError("The Program is locked, no new subsystems can be created.")
if not isinstance(n, numbers.Integral) or n < 1:
raise ValueError("Number of added subsystems {} is not a positive integer.".format(n))
first_unassigned_index = len(self.reg_refs)
# create a list of RegRefs
inds = [first_unassigned_index + i for i in range(n)]
refs = tuple(RegRef(i) for i in inds)
# add them to the index map
for r in refs:
self.reg_refs[r.ind] = r
# all the newly reserved indices are unused for now
self.unused_indices.update(inds)
return refs
def _delete_subsystems(self, refs):
"""Delete existing subsystem references.
To avoid discrepancies with the backend this method must not be called directly,
but rather indirectly by using :class:`~strawberryfields.ops._Delete` instances
in the Program context.
Args:
refs (Sequence[RegRef]): subsystems to delete
"""
# NOTE: refs have already been through _test_regrefs() in append() and thus should be valid
for r in refs:
# mark the RegRef as deleted
r.active = False
# self.reg_refs[r.ind].active = False
# NOTE: deleted indices are *not* removed from self.unused_indices
[docs] def lock(self):
"""Finalize the program.
When a Program is locked, no more Commands can be appended to it.
The locking happens when the program is run, compiled, or a successor Program is constructed,
in order to ensure that the RegRef state of the Program does not change anymore.
"""
self.locked = True
[docs] def can_follow(self, prev):
"""Check whether this program can follow the given program.
This requires that the final RegRef state of the first program matches
the initial RegRef state of the second program, i.e., they have the same number
number of RegRefs, all with identical indices and activity states.
Args:
prev (Program): preceding program fragment
Returns:
bool: True if the Program can follow prev
"""
# TODO NOTE unused_indices is not compared here, in order to allow program fragment repetition
return self.init_reg_refs == prev.reg_refs
def _index_to_regref(self, ind):
"""Try to find a RegRef corresponding to a given subsystem index.
Args:
ind (int): subsystem index
Returns:
RegRef: corresponding register reference
Raises:
.RegRefError: if the subsystem cannot be found, or is invalid
"""
# index must be found in the dict
if ind not in self.reg_refs:
raise RegRefError("Subsystem {} does not exist.".format(ind))
return self.reg_refs[ind]
def _test_regrefs(self, reg):
"""Make sure reg is a valid selection of subsystems, convert them to RegRefs.
A register reference is valid if it is properly recorded in self.reg_refs
and has not been deleted. The selection is valid if it contains only
valid RegRefs and no subsystem is repeated.
Args:
reg (Iterable[int, RegRef]): subsystem references
Returns:
list[RegRef]: converted subsystem references
Raises:
.RegRefError: if an invalid subsystem reference is found
"""
temp = []
for rr in reg:
# must be either an integer or a RegRef
if isinstance(rr, RegRef):
# regref must be found in the dict values (the RegRefs are compared using __eq__, which, since we do not define it, defaults to "is")
if rr not in self.reg_refs.values():
raise RegRefError("Unknown RegRef.")
if self.reg_refs[rr.ind] is not rr:
raise RegRefError("RegRef state has become inconsistent.")
elif isinstance(rr, numbers.Integral):
rr = self._index_to_regref(rr)
else:
raise RegRefError("Subsystems can only be indexed using integers and RegRefs.")
if not rr.active:
raise RegRefError("Subsystem {} has already been deleted.".format(rr.ind))
if rr in temp:
raise RegRefError("Trying to act on the same subsystem more than once.")
temp.append(rr)
return temp
[docs] def append(self, op, reg):
"""Append a command to the program.
Args:
op (Operation): quantum operation
reg (list[int, RegRef]): register subsystem(s) to apply it to
Returns:
list[RegRef]: subsystem list as RegRefs
"""
if self.locked:
raise CircuitError("The Program is locked, no more Commands can be appended to it.")
# test that the target subsystem references are ok
reg = self._test_regrefs(reg)
# also test possible Parameter-related dependencies
self._test_regrefs(op.measurement_deps)
for rr in reg:
# it's used now
self.unused_indices.discard(rr.ind)
self.circuit.append(Command(op, reg))
return reg
def _is_select(self, op):
"""Check if the quantum operation uses post-selection.
A quantum operation uses post-selection if it has the attribute ``select``
and this attribute is not ``None``.
Args:
op (Operation): quantum operation
Returns:
bool: whether the quantum operation uses post-selection or not
"""
return hasattr(op, "select") and getattr(op, "select") is not None
def _is_feed_forward(self, op):
"""Check if the quantum operation uses feed-forwarding.
A quantum operation uses feed-forwarding if its attribute
``measurement_deps`` is a non empty set.
Args:
op (Operation): quantum operation
Returns:
bool: whether the quantum operation uses feed-forwarding or not
"""
return len(op.measurement_deps) != 0
@property
def has_post_selection(self):
"""Indicate if any operation in the program uses post-selection or not.
Returns:
bool: whether post-selection is used anywhere in the circuit
"""
is_select_ops = [self._is_select(c.op) for c in self.circuit]
return any(is_select_ops)
@property
def has_feed_forward(self):
"""Indicate if any operation in the program uses feed-forwarding or not.
Returns:
bool: whether feed-forwarding is used anywhere in the circuit
"""
is_feed_forward_ops = [self._is_feed_forward(c.op) for c in self.circuit]
return any(is_feed_forward_ops)
def _linked_copy(self):
"""Create a copy of the Program, linked to the original.
Both the original and the copy are :meth:`locked <lock>`, since they share their RegRefs.
FreeParameters are also shared.
Returns:
Program: a copy of the Program
"""
self.lock()
p = copy.copy(self)
for name, val in self.__dict__.items():
# Deep-copy all attributes except 'circuit' and 'reg_refs', since the programs
# should share the same register references. Program.circuit potentially
# contains FreeParameters/MeasuredParameters, which contain RegRefs.
if name not in ("circuit", "reg_refs", "init_reg_refs", "free_params"):
setattr(p, name, copy.deepcopy(val))
# link to the original source Program
if self.source is None:
p.source = self
else:
p.source = self.source
return p
[docs] def assert_modes(self, device):
"""Check that the number of modes in the program is valid for the given device.
.. note::
``device.modes`` must be an integer with the allowed number of modes
for the target, or a dictionary containing the maximum number of allowed
measurements for the specified target.
Args:
device (.strawberryfields.Device): device specification object to use
"""
# Program subsystems may be created and destroyed during execution. The length
# of the program registers represents the total number of modes that has ever existed.
modes_total = len(self.reg_refs)
if isinstance(device.modes, int):
if modes_total > device.modes:
raise CircuitError(
f"This program contains {modes_total} modes, but the device '{device.target}' "
f"only supports a {device.modes}-mode program."
)
return
# from here on `device.modes` is assumed to be a dictionary
num_pnr, num_homodyne, num_heterodyne = 0, 0, 0
try:
max_pnr = device.modes["pnr_max"]
max_homodyne = device.modes["homodyne_max"]
max_heterodyne = device.modes["heterodyne_max"]
except (KeyError, TypeError) as e:
device_modes = device.modes
if isinstance(device.modes, dict):
device_modes = set(device.modes.keys())
raise KeyError(
"Expected keys for the maximum allowed number of PNR ('pnr_max'), homodyne "
"('homodyne_max'), and heterodyne ('heterodyne_max') measurements. Got keys "
f"{device_modes}"
) from e
for c in self.circuit:
op_name = str(c.op)
if "MeasureFock" in op_name:
num_pnr += len(c.reg)
elif "MeasureHomodyne" in op_name or "MeasureX" in op_name or "MeasureP" in op_name:
num_homodyne += len(c.reg)
elif "MeasureHeterodyne" in op_name or "MeasureHD" in op_name:
num_heterodyne += len(c.reg)
if num_pnr > max_pnr:
raise CircuitError(
f"This program contains {num_pnr} fock measurements. "
f"A maximum of {max_pnr} fock measurements are supported."
)
if num_homodyne > max_homodyne:
raise CircuitError(
f"This program contains {num_homodyne} homodyne measurements. "
f"A maximum of {max_homodyne} homodyne measurements are supported."
)
if num_heterodyne > max_heterodyne:
raise CircuitError(
f"This program contains {num_heterodyne} heterodyne measurements. "
f"A maximum of {max_heterodyne} heterodyne measurements are supported."
)
[docs] def compile(self, *, device=None, compiler=None, **kwargs):
"""Compile the program given a Strawberry Fields photonic compiler, or
hardware device specification.
The compilation process can involve up to three stages:
1. **Validation:** Validates properties of the program, including number of modes and
allowed operations, making sure all the :doc:`/introduction/ops` used are accepted by the
compiler.
2. **Decomposition:** Once the program has been validated, decomposition are performed,
transforming certain gates into sequences of simpler gates.
3. **General compilation:** Finally, the compiler might specify bespoke compilation logic
for transforming the quantum circuit into an equivalent circuit which can be executed
by the target device.
**Example:**
The ``gbs`` compile target will
compile a circuit consisting of Gaussian operations and Fock measurements
into canonical Gaussian boson sampling form.
>>> prog2 = prog.compile(compiler="gbs")
For a hardware device a :class:`~.Device` object, and optionally a specified compile strategy,
must be supplied. If no compile strategy is supplied the default compiler from the device
specification is used.
>>> eng = sf.RemoteEngine("X8")
>>> device = eng.device_spec
>>> prog2 = prog.compile(device=device, compiler="Xcov")
Args:
device (~strawberryfields.Device): device specification object to use for
program compilation
compiler (str, ~strawberryfields.compilers.Compiler): Compiler name or compile strategy
to use. If a device is specified, this overrides the compile strategy specified by
the hardware :class:`~.Device`.
Keyword Args:
optimize (bool): If True, try to optimize the program by merging and canceling gates.
The default is False.
warn_connected (bool): If True, the user is warned if the quantum circuit is not weakly
connected. The default is True.
shots (int): Number of times the program measurement evaluation is repeated. Passed
along to the compiled program's ``run_options``.
Returns:
Program: compiled program
"""
# pylint: disable=too-many-branches
if device is None and compiler is None:
raise ValueError("Either one or both of 'device' and 'compiler' must be specified")
def _get_compiler(compiler_or_name):
if compiler_or_name in compiler_db:
return compiler_db[compiler_or_name]()
if isinstance(compiler_or_name, Compiler):
return compiler_or_name
raise ValueError(f"Unknown compiler '{compiler_or_name}'.")
if device is not None:
target = device.target
if compiler is None:
# get the default compiler from the device spec
compiler = compiler_db[device.default_compiler]()
else:
compiler = _get_compiler(compiler)
if device.modes is not None:
# check that the number of modes is correct, if device.modes is provided
# as an integer, or that the number of measurements is within the allowed
# limits for each measurement type, if `device.modes` is a dictionary
self.assert_modes(device)
# if a device layout exist and the device default compiler is the same as
# the requested compiler, initialize the circuit in the compiler class
if device.layout and device.default_compiler == compiler.short_name:
compiler.init_circuit(device.layout)
else:
compiler = _get_compiler(compiler)
target = compiler.short_name
seq = compiler.decompose(self.circuit)
if kwargs.get("warn_connected", True):
DAG = pu.list_to_DAG(seq)
temp = nx.algorithms.components.number_weakly_connected_components(DAG)
if temp > 1:
warnings.warn("The circuit consists of {} disconnected components.".format(temp))
# run optimizations
if kwargs.get("optimize", False):
seq = pu.optimize_circuit(seq)
compiled = self._linked_copy()
seq = compiler.compile(seq, self.register)
# create the compiled Program
compiled.circuit = seq
compiled._target = target # pylint: disable=protected-access
compiled._compile_info = (device, compiler.short_name) # pylint: disable=protected-access
# parameters are updated if necessary due to compiler changes
compiler.update_params(compiled, device)
# Get run options of compiled program.
run_options = {k: kwargs[k] for k in ALLOWED_RUN_OPTIONS if k in kwargs}
compiled.run_options.update(run_options)
# set backend options of the program
backend_options = {k: kwargs[k] for k in kwargs if k not in ALLOWED_RUN_OPTIONS}
compiled.backend_options.update(backend_options)
if kwargs.get("realistic_loss", False):
try:
compiler.add_loss(compiled, device)
except NotImplementedError:
warnings.warn(f"Compiler {compiler} does not support adding realistic loss.")
# if device spec has allowed gate parameters, validate the applied gate parameters
if device and device.gate_parameters:
if not device.layout:
raise ValueError(
"Gate parameters cannot be validated. Device specification is missing a "
"circuit layout."
)
bb_device = bb.loads(device.layout)
# if there is no target in the layout, set the device target in the Blackbird program
if bb_device.target["name"] is None:
bb_device._target["name"] = device.target # pylint: disable=protected-access
lossless_compiled = compiled._linked_copy()
lossless_compiled.circuit = remove_loss(compiled.circuit)
bb_compiled = sf.io.to_blackbird(lossless_compiled)
try:
user_parameters = match_template(bb_device, bb_compiled)
except TemplateError as e:
raise CircuitError(
"Program cannot be used with the compiler '{}' "
"due to incompatible topology.".format(compiler.short_name)
) from e
device.validate_parameters(**user_parameters)
return compiled
[docs] def optimize(self):
"""Simplify and optimize the program.
The simplifications are based on the algebraic properties of the gates,
e.g., combining two consecutive gates of the same gate family.
Returns a copy of the program, sharing RegRefs with the original.
See :func:`~strawberryfields.program_utils.optimize_circuit`.
Returns:
Program: optimized copy of the program
"""
opt = self._linked_copy()
opt.circuit = pu.optimize_circuit(self.circuit)
return opt
[docs] def draw_circuit(self, tex_dir="./circuit_tex", write_to_file=True):
r"""Draw the circuit using the Qcircuit :math:`\LaTeX` package.
This will generate the LaTeX code required to draw the quantum circuit
diagram corresponding to the Program.
The drawing of the following Xanadu supported operations are currently supported:
.. rst-class:: docstable
+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Gate type | Supported gates |
+===================+========================================================================================================================================================================+
| Single mode gates | :class:`~.Dgate`, :class:`~.Xgate`, :class:`~.Zgate`, :class:`~.Sgate`, :class:`~.Rgate`, :class:`~.Pgate`, :class:`~.Vgate`, :class:`~.Kgate`, :class:`~.Fouriergate` |
+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Two mode gates | :class:`~.BSgate`, :class:`~.S2gate`, :class:`~.CXgate`, :class:`~.CZgate`, :class:`~.CKgate` |
+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
.. note::
Measurement operations :class:`~.MeasureHomodyne`, :class:`~.MeasureHeterodyne`,
and :class:`~.MeasureFock` are not currently supported.
Args:
tex_dir (str): relative directory for latex document output
write_to_file (bool): if False, no output file is created
Returns:
list[str]: filename of the written tex document and the written tex content
"""
drawer = sfcd.Circuit(wires=self.init_num_subsystems)
self.print(drawer.parse_op)
tex = drawer.dump_to_document()
document = None
if write_to_file:
document = drawer.compile_document(tex_dir=tex_dir)
return [document, tex]
@property
def target(self):
"""The target specification the program has been compiled against.
If the program has not been compiled, this will return ``None``.
Returns:
str or None: the short name of the target Compiler template if
compiled, otherwise None
"""
return self._target
@property
def compile_info(self):
"""The device specification and the compiler that was used during
compilation.
If the program has not been compiled, this will return ``None``.
Returns:
tuple or None: device specification and the short name of the
Compiler that was used if compiled, otherwise None
"""
return self._compile_info
[docs] def params(self, *args):
"""Create and access free circuit parameters.
Returns the named free parameters. If a parameter does not exist yet, it is created and returned.
Args:
*args (tuple[str]): name(s) of the free parameters to access
Returns:
FreeParameter, list[FreeParameter]: requested parameter(s)
"""
ret = []
for a in args:
if not isinstance(a, str):
raise TypeError("Parameter names must be strings.")
if a not in self.free_params:
if self.locked:
raise CircuitError(
"The Program is locked, no more free parameters can be created."
)
p = FreeParameter(a)
self.free_params[a] = p
else:
p = self.free_params[a]
ret.append(p)
if len(ret) == 1:
return ret[0]
return ret
[docs] def bind_params(self, binding):
"""Binds the free parameters of the program to the given values.
Args:
binding (dict[Union[str, FreeParameter], Any]): mapping from parameter names (or the
parameters themselves) to parameter values
Raises:
ParameterError: tried to bind an unknown parameter
"""
for k, v in binding.items():
temp = self.free_params.get(k) # it's a name
if temp:
temp.val = v
elif k in self.free_params.values(): # it's a parameter
k.val = v
else:
raise ParameterError("Unknown free parameter '{}'".format(k))
_modules/strawberryfields/program
Download Python script
Download Notebook
View on GitHub