import pennylane as qml
from pennylane import numpy as qnp
from pennylane import math
from .network_nodes import *
[docs]
class NetworkAnsatz:
"""The ``NetworkAnsatz`` class describes a parameterized quantum network.
:param layers: Each layer represents a chronological step in the network simulation.
The nodes in each layer apply their operations in parallel where no two nodes can
operate on the same wire.
:type layers: list[list[:class:`qnetvo.NetworkNode`]]
:param dev_kwargs: Keyword arguments for the `pennylane.device`_ function.
:type dev_kwargs: *optional* dictionary
.. _pennylane.device: https://pennylane.readthedocs.io/en/stable/code/api/pennylane.device.html
:returns: An instantiated ``NetworkAnsatz``.
There are some conventions that should be followed when defining network layers:
1. The first layer should contain all :class:`qnetvo.PrepareNode` s in the network.
2. The last layer should contain all :class:`qnetvo.MeasureNode` s in the network.
3. If classical communication is considered, then a :class:`qnetvo.CCSenderNode` must be used
to obtain the communicated values in a layer preceding the layers where :class:`qnetvo.CCReceiverNode` s
consume the classical communication.
ATTRIBUTES:
* **layers** - ``list[list[NetworkNode]]``, The input layers of network nodes.
* **layers_wires** - ``list[qml.wires.Wires]``, The wires used for each layer.
* **layers_cc_wires_in** - ``list[qml.wires.Wires]``, The classical communication wires input to each network layer.
* **layers_cc_wires_out** - ``list[qml.wires.Wires]``, The classical communication wires output from each network layer.
* **layers_num_settings** - ``list[int]``, The number of setting used in each layer.
* **layers_total_num_in** - ``list[int]``, The total number of inputs for each layer.
* **layers_node_num_in** - ``list[list[int]]``, The number of inputs for each node in the layer.
* **layers_num_nodes** - ``list[int]``, The number of nodes in each layer.
* **network_wires** - The list of wires used by the network ansatz.
* **network_cc_wires** - The list of classical communication wires in the network.
* **num_cc_wires** - The number of classical communication wires.
* **dev_kwargs** - *mutable*, the keyword args to pass to the `pennylane.device`_ function.
If no ``dev_kwargs`` are provided, a ``"default.qubit"`` is constructed for
noiseless networks.
* **fn** (*function*) - A quantum function implementing the quantum network ansatz.
* **parameter_partitions** (*List[List[List[Tuple]]]*) - A ragged array containing tuples that specify
how to partition a 1D array of network settings into the subset of settings
passed to the qnode simulating the network. See
:meth:`get_network_parameter_partitions` for details.
:raises ValueError: If the ``wires`` are not unique across all nodes in an ansatz layer or the ``cc_wires_out`` are not unique across all layers.
:raises ValueError: If ``cc_wires_in`` are used by a layer preceding the classical values output onto ``cc_wires_out``.
"""
def __init__(self, *layers, dev_kwargs=None):
self.layers = layers
# layers attributes
self.layers_wires = [self.collect_wires([node.wires for node in layer]) for layer in layers]
self.layers_cc_wires_in = [
self.collect_wires([node.cc_wires_in for node in layer], check_unique=False)
for layer in layers
]
self.layers_cc_wires_out = [
self.collect_wires([node.cc_wires_out for node in layer]) for layer in layers
]
self.check_cc_causal_structure(self.layers_cc_wires_in, self.layers_cc_wires_out)
self.layers_num_settings = [
math.sum([node.num_settings for node in layer]) for layer in layers
]
self.layers_total_num_in = [math.prod([node.num_in for node in layer]) for layer in layers]
self.layers_node_num_in = [[node.num_in for node in layer] for layer in self.layers]
self.layers_num_nodes = [len(layer) for layer in self.layers]
# network attributes
self.network_wires = qml.wires.Wires.all_wires(self.layers_wires)
self.network_cc_wires = self.collect_wires(
[node.cc_wires_out for layer in layers for node in layer]
)
self.num_cc_wires = len(self.network_cc_wires)
# device attributes
default_dev_name = "default.qubit"
self.dev_kwargs = dev_kwargs or {"name": default_dev_name}
self.dev_kwargs["wires"] = self.network_wires
# ansatz function attributes
self.fn = self.ansatz_circuit_fn()
self.parameter_partitions = self.get_network_parameter_partitions()
def __call__(self, settings=[]):
self.fn(settings)
def ansatz_circuit_fn(self):
layer_fns = [self.circuit_layer_fn(layer_nodes) for layer_nodes in self.layers]
def ansatz_circuit(settings=[]):
cc_wires = [None] * self.num_cc_wires
start_id = 0
for i, layer_fn in enumerate(layer_fns):
end_id = start_id + self.layers_num_settings[i]
layer_settings = settings[start_id:end_id]
layer_fn(layer_settings, cc_wires)
start_id = end_id
return ansatz_circuit
[docs]
@staticmethod
def circuit_layer_fn(layer_nodes):
"""Constructs a quantum function for an ansatz layer of provided network nodes.
:param layer_nodes: A list of nodes in a network layer.
:type layer_nodes: list[:class:`qnetvo.NetworkNode`]
:returns: A quantum function evaluated as ``circuit_layer(settings)`` where ``settings``
is an array constructed via the ``layer_settings`` function.
:rtype: function
"""
def circuit_layer(settings, cc_wires):
start_id = 0
for node in layer_nodes:
end_id = start_id + node.num_settings
node_settings = settings[start_id:end_id]
node_cc_wires = [cc_wires[i] for i in node.cc_wires_in]
if isinstance(node, CCSenderNode):
cc_out = node(node_settings, node_cc_wires)
for i in range(len(cc_out)):
cc_wires[node.cc_wires_out[i]] = cc_out[i]
else:
node(node_settings, node_cc_wires)
start_id = end_id
return circuit_layer
[docs]
def get_network_parameter_partitions(self):
"""
A nested list containing tuples that specify how to partition a 1D array
of network settings into the subset of settings passed to the qnode simulating
the network. Each tuple ``(start_id, stop_id)`` is indexed by ``layer_id``,
``node_id``, and classical ``input_id`` as
``parameter_partitions[layer_id][node_id][input_id] => (start_id, stop_id)``.
The ``start_id`` and ``stop_id`` describe the slice ``network_settings[start_id:stop_id]``.
"""
parameter_partitions = []
start_id = 0
for i, layer in enumerate(self.layers):
parameter_partitions += [[]]
for j, node in enumerate(layer):
parameter_partitions[i] += [[]]
for _ in range(node.num_in):
stop_id = start_id + node.num_settings
parameter_partitions[i][j] += [(start_id, stop_id)]
start_id = stop_id
return parameter_partitions
[docs]
@staticmethod
def collect_wires(wires_lists, check_unique=True):
"""A helper method for the ``NetworkAnsatz`` class which collects and aggregates the
wires from a set of collection of network nodes (``prepare_nodes`` or ``measure_nodes``).
:param network_nodes: A list consisting of either ``PrepareNode``'s or ``MeasureNode``'s.
:type network_nodes: list[PrepareNode or MeasureNode]
:raises ValueError: If the same wire is used in two different nodes in ``network_nodes``.
"""
wires_objs = list(map(qml.wires.Wires, wires_lists))
all_wires = qml.wires.Wires.all_wires(wires_objs)
if check_unique:
unique_wires = qml.wires.Wires.unique_wires(wires_objs)
# two nodes cannot prepare a state on the same wire
if not all_wires.tolist() == unique_wires.tolist():
raise ValueError(
"One or more wires are not unique. Each node must contain unique wires."
)
return all_wires
[docs]
@staticmethod
def check_cc_causal_structure(cc_wires_in_layers, cc_wires_out_layers):
"""Verifies that the classical communication is causal.
Note that ``cc_wires_out`` describes the classical communication senders while ``cc_wires_in`` describe
classical communication receivers.
All network ansatzes must have a causal structure where nodes output their classical communication in
layers that precede the nodes that use that classical communication.
:params cc_wires_in_layers: The classical communication input wires, ``cc_wires_in``, considered for each layer.
:type cc_wires_in_layers: list[qml.wires.Wires]
:params cc_wires_out_layers: The classical communication output wires, ``cc_wires_out``, considered for each layer.
:type cc_wires_out_layers: list[qml.wires.Wires]
:returns: ``True`` if the
:raises ValueError: If ``cc_wires_in`` are used by a layer preceding the classical values output onto ``cc_wires_out``.
"""
num_layers = len(cc_wires_in_layers)
for i in range(num_layers):
measured_cc_wires = (
qml.wires.Wires.all_wires([measured_cc_wires, cc_wires_out_layers[i - 1]])
if (i - 1) >= 0
else qml.wires.Wires([])
)
if not measured_cc_wires.contains_wires(cc_wires_in_layers[i]):
raise ValueError(
"The `cc_wires_in` of layer "
+ str(i)
+ " do not have corresponding `cc_wires_out` in a preceding layer."
)
return True
[docs]
def layer_settings(self, network_settings, layer_id, layer_inputs):
"""Constructs the list of settings for a circuit layer in the network ansatz.
:param network_settings: A list containing the circuit settings for each node
in the network.
:type network_settings: List[Float]
:param layer_id: The id for the targeted layer.
:type layer_id: Int
:param layer_inputs: A list of the classical inputs supplied to each network node.
:type layer_inputs: List[Int]
:returns: A 1D array of settings for the circuit layer.
:rtype: List[float]
"""
settings = []
for j, node_input in enumerate(layer_inputs):
start_id, stop_id = self.parameter_partitions[layer_id][j][node_input]
settings += [network_settings[k] for k in range(start_id, stop_id)]
return settings
[docs]
def qnode_settings(self, network_settings, network_inputs):
"""Constructs a list of settings to pass to the qnode executing the network ansatz.
:param network_settings: The settings for the network ansatz scenario.
:type network_settings: list[list[np.ndarray]]
:param network_inputs: The classical inputs passed to each network node.
:type network_inputs: List[List[int]]
:returns: A list of settings to pass to the constructed qnode.
:rtype: np.array
"""
settings = []
for i, layer_inputs in enumerate(network_inputs):
settings += self.layer_settings(network_settings, i, layer_inputs)
return qml.math.stack(settings)
[docs]
def expand_qnode_settings(self, qn_settings, network_inputs):
"""Constructs network settings from qnode settings and the network inputs.
This implements the reverse mapping implemented in :meth:`qnetvo.NetworkAnsatz.qnode_settings`.
Since there are fewer qnode settings than network settings, empty elements in the returned
list are set to zero.
:param qn_settings: Settings to pass to the network qnode.
:type qn_settings: array[float]
:param network_inputs: The considered classical network inputs.
:type network_inputs: list[int]
:returns: The network settings
:rtype: array[float]
"""
expanded_settings = self.zero_network_settings()
qn_start_id = 0
for i, layer_inputs in enumerate(network_inputs):
for j, node_input in enumerate(layer_inputs):
node = self.layers[i][j]
qn_stop_id = qn_start_id + node.num_settings
start_id, stop_id = self.parameter_partitions[i][j][node_input]
expanded_settings[start_id:stop_id] = qn_settings[qn_start_id:qn_stop_id]
qn_start_id = qn_stop_id
return qml.math.stack(expanded_settings)
[docs]
def rand_network_settings(self, fixed_setting_ids=[], fixed_settings=[]):
"""Creates an array of randomized differentiable settings for the network ansatz.
If fixed settings are specified, then they are marked as ``requires_grad=False`` and
not differentatiated during optimzation.
:param fixed_setting_ids: The ids of settings that are held constant during optimization.
Also requires `fixed_settings` to be provided.
:type fixed_setting_ids: *optional* List[Int]
:param fixed_settings: The constant values for fixed settings.
:type fixed_settings: *optional* List[Float]
:returns: A 1D list of ``qnp.tensor`` scalar values having ``requires_grad=True``.
:rtype: List[Float]
"""
num_settings = self.parameter_partitions[-1][-1][-1][-1]
rand_settings = [
qnp.array(2 * qnp.pi * qnp.random.rand() - qnp.pi) for _ in range(num_settings)
]
if len(fixed_setting_ids) > 0 and len(fixed_settings) > 0:
for i, id in enumerate(fixed_setting_ids):
rand_settings[id] = qnp.array(fixed_settings[i], requires_grad=False)
return rand_settings
[docs]
def tf_rand_network_settings(self, fixed_setting_ids=[], fixed_settings=[]):
"""Creates a randomized settings array for the network ansatz using TensorFlow
tensor types.
:param fixed_setting_ids: The ids of settings that are held constant during optimization.
Also requires `fixed_settings` to be provided.
:type fixed_setting_ids: *optional* List[Int]
:param fixed_settings: The constant values for fixed settings.
:type fixed_settings: *optional* List[Float]
:returns: A 1D list of ``tf.Variable`` and ``tf.constant`` scalar values.
:rtype: List[tf.Tensor]
"""
from .lazy_tensorflow_import import tensorflow as tf
np_settings = self.rand_network_settings(fixed_setting_ids, fixed_settings)
return [
tf.Variable(setting) if qml.math.requires_grad(setting) else tf.constant(setting)
for setting in np_settings
]
[docs]
def zero_network_settings(self):
"""Creates a settings array for the network ansatz that consists of zeros.
:returns: A 1D list of ``np.tensor`` scalar values having ``requires_grad=True``.
:rtype: List[Float]
"""
num_settings = self.parameter_partitions[-1][-1][-1][-1]
return [qnp.array(0, requires_grad=True) for _ in range(num_settings)]