# Source code for strawberryfields.compilers.gaussian_merge

# Copyright 2019-2021 Xanadu Quantum Technologies Inc.

# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# Unless required by applicable law or agreed to in writing, software
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
"""This module contains a compiler that merges Gaussian operations into their symplectic forms,
in a Gaussian and non-Gaussian circuit."""

import strawberryfields.program_utils as pu

from .compiler import Compiler
from .gaussian_unitary import GaussianUnitary

def get_qumodes_operated_upon(op):
"""
Helper function that returns list of integers, which are the qumode indexes that are operated by op.
"""
return [reg.ind for reg in op.reg]

def get_op_name(op):
"""
Helper function that obtains the string name of an operation.
"""
return op.op.__class__.__name__

[docs]class GaussianMerge(Compiler):
"""Compiler that merges adjacent Gaussian operations into a single symplectic transformation,
to reduce the depth of non-Gaussian programs.

As a result, the Gaussian operations that this compiler returns are :class:~.ops.GaussianTransform,
and :class:~.ops.Displacement. Meanwhile, non-Gaussian operations remain unchanged.

**Example:**

Consider the following Strawberry Fields program, compiled using the 'gaussian_merge' compiler:

.. code-block:: python3

from strawberryfields.ops import Xgate, Zgate, BSgate, Rgate
import strawberryfields as sf

circuit = sf.Program(1)
with circuit.context as q:
Rgate(0.6)|q
Rgate(0.2)|q
Kgate(0.4)|q
Rgate(0.1)|q
Rgate(0.6)|q
Dgate(0.01)|(q)

compiled_circuit = circuit.compile(compiler="gaussian_merge")

We can now print the compiled circuit, which has merged adjacent Gaussian
operations into singular :class:~.GaussianTransform operations:

>>> compiled_circuit.print()
GaussianTransform([[ 0.6967 -0.7174]
[0.7174  0.6967]]) | (q)
Kgate(0.4)|q
GaussianTransform([[ 0.7648 -0.6442]
[0.6442  0.7648]]) | (q)
Dgate(0.01, 0) | (q)
"""

short_name = "gaussian_merge"
interactive = True

primitives = {
# meta operations
"All",
"_New_modes",
"_Delete",
# state preparations
"Ket",
# measurements
"MeasureFock",
"MeasureHomodyne",
# single mode gates
"Dgate",
"Sgate",
"Rgate",
"Vgate",
"Kgate",
# two mode gates
"MZgate",
"sMZgate",
"BSgate",
"CKgate",
"S2gate",
"Interferometer",
"GaussianTransform",
}

decompositions = {
"GraphEmbed": {},
"BipartiteGraphEmbed": {},
"Gaussian": {},
"Pgate": {},
"CXgate": {},
"CZgate": {},
"Xgate": {},
"Zgate": {},
"Fouriergate": {},
}

prep_states = ["Ket"]

gaussian_ops = [
"Dgate",
"BSgate",
"S2gate",
"Sgate",
"GaussianTransform",
"Rgate",
"Interferometer",
"MZgate",
"sMZgate",
]

def __init__(self):
self.curr_seq = None
self.DAG = None
self.new_DAG = None

[docs]    def compile(self, seq, registers):
"""Attempt to merge Gaussian operations into Gaussian Transforms in a hybrid program.

Args:
seq (Sequence[Command]): quantum circuit to modify
registers (Sequence[RegRefs]): quantum registers
Returns:
List[Command]: modified circuit
"""
self.curr_seq = seq
while self.merge_a_gaussian_op(registers):
continue
return self.curr_seq

[docs]    def merge_a_gaussian_op(self, registers):
"""
Main function to merge a gaussian operation with its gaussian neighbours.
If merge is achieved, the method updates self.curr_seq and returns True
else (merge cannot be achieved), the method returns False.

Program Flow:
- For each operation (op) check and obtain Gaussian operations that can be merged
(get_valid_gaussian_merge_ops).
- If the operation has successor gaussian operations that can be merged,
then merge them using gaussian_unitary.py.
- Determine displacement gates, from gaussian unitary merge, and map them to the qumodes acted upon
- Attach predecessor operations of the main operation (op) to new Gaussian transform operations.
- Attach successor non Gaussian operations of op to a displacement gate, if present,
or a gaussian transform operation from the merged operations (add_non_gaussian_successor_gates).
- Attach all non-merged predecessor and successor of the merged operations to the new gaussian
- Remove nodes of operations that were merged in and convert DAG to sequence.

"""
self.DAG = pu.list_to_DAG(self.curr_seq)

for op in list(self.DAG.nodes):
successors = list(self.DAG.successors(op))
predecessors = list(self.DAG.predecessors(op))
# If operation is a Gaussian operation
if get_op_name(op) in self.gaussian_ops:
merged_gaussian_ops = self.get_valid_gaussian_merge_ops(op)

# If there are successor operations that are Gaussian and can be merged
if merged_gaussian_ops:
self.new_DAG = self.DAG.copy()
# Fix order of operations
unified_operations = self.organize_merge_ops([op] + merged_gaussian_ops)
gaussian_transform = GaussianUnitary().compile(unified_operations, registers)

# Logic to add displacement gates. Returns a dictionary,
# where the value is a displacement gate added and its key is the qumode its operating upon.

# If there are predecessors: Attach predecessor edges to new gaussian transform
if predecessors:
[(pre, gaussian_transform) for pre in predecessors]
)

# Add edges to all successor operations not merged
gaussian_transform, successors, displacement_mapping
)

# Add edges for all successor/predecessor operations of the merged operations
gaussian_transform, merged_gaussian_ops, displacement_mapping
)

self.new_DAG.remove_nodes_from([op] + merged_gaussian_ops)

self.curr_seq = pu.DAG_to_list(self.new_DAG)
return True
return False

[docs]    def recursive_d_gate_successors(self, gate):
"""
Gets all displacement gates in channel if they follow each other. Returns lists of displacement gates.
"""
d_gates = []
successors = list(self.DAG.successors(gate))
for successor in successors:
if "Dgate" in get_op_name(successor):
d_gates.append(successor)
ret = self.recursive_d_gate_successors(successor)
if ret:
d_gates += ret
return d_gates

self, gaussian_transform, successors, displacement_mapping
):
"""
Updates the DAG by adding edges between new gaussian transform and non-gaussian operations
from original operations.
"""
for successor_op in successors:
if get_op_name(successor_op) not in self.gaussian_ops:
# If there are no displacement gates.
# Add edges from it to successor gates if they act upon the same qumodes
if not displacement_mapping:
# Add edge from gaussian transform to successor operation

self, gaussian_transform, merged_gaussian_ops, displacement_mapping
):
"""
Updated DAG by adding edges between gaussian transform/displacement operations to unmerged gaussian operations.
"""
for gaussian_op in merged_gaussian_ops:
# Need special logic if there are displacement gates
if displacement_mapping:
for successor_op in self.DAG.successors(gaussian_op):
placed_edge = False
successor_op_qumodes = get_qumodes_operated_upon(successor_op)
for qumode in successor_op_qumodes:
# If displacement gate operates on the same qumodes as the non-gaussian operation then don't
# add an edge. If register operated upon by successor operation has a displacement gate, add edge.
if (
qumode in displacement_mapping
and qumode not in self.non_gaussian_qumodes_dependecy(successor_op)
):
placed_edge = True

if not placed_edge:
else:
[(gaussian_transform[-1], post) for post in self.DAG.successors(gaussian_op)]
)

for gaussian_op in merged_gaussian_ops:
# Append Predecessors to Gaussian Transform
for predecessor in self.DAG.predecessors(gaussian_op):
# Make sure adding the edge wont make a cycle

"""
Adds displacement gates to new DAG and returns dict with the following format:
{1: Dgate|q, 2:Dgate|q}
"""
displacement_mapping = {}
#  The Gaussian Unitary compiler can return Dgate in two ways. Either there is a Gaussian Transform plus Dgates
#  or the only thing returned is a Dgate.
if len(gaussian_transform) > 1 or "Dgate" in get_op_name(gaussian_transform):
for displacement_gate in gaussian_transform[1:]:
displacement_mapping[displacement_gate.reg.ind] = displacement_gate
return displacement_mapping

[docs]    def get_valid_gaussian_merge_ops(self, op):
"""
Obtains the valid gaussian operations that can be merged with op at the current DAG configuration.
"""
merged_gaussian_ops = []
for successor_op in self.DAG.successors(op):
# If successor operation is a Gaussian operation append to list for merging
if get_op_name(successor_op) in self.gaussian_ops:
merged_gaussian_ops.append(successor_op)
# Get displacement operations (recursively) that follow after successor operation
d_gate_successors = self.recursive_d_gate_successors(successor_op)
if d_gate_successors:
merged_gaussian_ops += d_gate_successors

# Add gaussian operations that should be exectued at the same time as op
# E.X Rgate|q Rgate|q -> BS|qq. Adds Rgate|q if Rgate|q is the op.
for gaussian_op in merged_gaussian_ops:
for predecessor in self.DAG.predecessors(gaussian_op):
if predecessor is op:
continue
if (
predecessor not in merged_gaussian_ops
and get_op_name(predecessor) in self.gaussian_ops
):
merged_gaussian_ops.append(predecessor)

merged_gaussian_ops = self.remove_invalid_operations(op, merged_gaussian_ops)

if self.is_redundant_merge(op, merged_gaussian_ops):
return []
return merged_gaussian_ops

[docs]    def is_redundant_merge(self, op, merged_gaussian_ops):
"""
Helper function that determines if merge will do nothing. i.e. just contains Gaussian Transforms and
Displacement operations.
"""
if "GaussianTransform" in get_op_name(op):
all_displacement_gates = True
for gate in merged_gaussian_ops:
if "Dgate" not in get_op_name(gate):
all_displacement_gates = False
if all_displacement_gates:
return True
return False

[docs]    def non_gaussian_qumodes_dependecy(self, op):
"""
Get qumodes used in predecessor non-gaussian operations of op. Returns a list of integers,
where the number depicts the qumode index that the non-Gaussian operation operates on.
"""
for predecessor in self.DAG.predecessors(op):
if not self.is_op_gaussian_or_prep(predecessor):
return get_qumodes_operated_upon(predecessor)
return []

[docs]    def organize_merge_ops(self, merged_gaussian_ops):
"""
Organize operations to be merged in order by using the order of the current operation sequence.
"""
organized_merge_operations = []
for op_in_seq in self.curr_seq:
if op_in_seq in merged_gaussian_ops:
organized_merge_operations.append(op_in_seq)
return organized_merge_operations

[docs]    def is_op_gaussian_or_prep(self, op):
"""
Helper function that returns True if op is Gaussian or a preparation state else returns False.
"""
op_name = get_op_name(op)
return op_name in self.gaussian_ops or op_name in self.prep_states

[docs]    def valid_prepend_op_addition(self, op, pre, merged_gaussian_ops):
"""
Helper function that ensures predecessor operation being added to merger list, did not skip any operations
between op (operation being merged) and pre (predecessor operation of op attempted to be merged).
"""
for pre_op in self.DAG.predecessors(op):
if pre_op not in merged_gaussian_ops:
pre_op_qumode = get_qumodes_operated_upon(pre_op)
if any(qumode in pre_op_qumode for qumode in get_qumodes_operated_upon(pre)):
return False
return True

[docs]    def remove_invalid_operations(self, op, merged_gaussian_ops):
"""
Helper function that removes operations from merged_gaussian_ops if they are being operated upon a
non-gaussian beforehand.
E.X  BS | q,q has successors Vgate | q & S2gate q,q in this case the S2gate is removed.
"""
op_qumodes = get_qumodes_operated_upon(op)
for gaussian_op in merged_gaussian_ops:
if any(
qumode in op_qumodes for qumode in self.non_gaussian_qumodes_dependecy(gaussian_op)
):
# Cannot merge gaussian ops that are operated upon a non-gaussian gate beforehand
# E.x. BS | q,q has successors V | q & S2gate q, q
merged_gaussian_ops.remove(gaussian_op)
return merged_gaussian_ops