XanaduAI / strawberryfields

Strawberry Fields is a full-stack Python library for designing, simulating, and optimizing continuous variable (CV) quantum optical circuits.
https://strawberryfields.ai
Apache License 2.0
754 stars 191 forks source link

DeviceSpec can have two different targets #449

Closed antalszava closed 2 years ago

antalszava commented 4 years ago

Issue description

Creating a DeviceSpec object involves specifying a target device at two distinct places:

  1. passing it as the target argument
  2. specifying within the circuit template, as the layout in the spec argument. Such a layout contains a target field, an excerpt could look as follows:
    name template_4x2_X8
    version 1.0
    target X8_01 (shots=1)

It is, however, not checked if these two values are equal. Therefore, in certain cases, different values are accepted, making it a (rare) edge case for DeviceSpec creation.

import strawberryfields as sf
import strawberryfields.ops as ops
from strawberryfields.utils import random_interferometer
U = random_interferometer(4)

from strawberryfields.api import DeviceSpec

import textwrap

test_spec = {
    "layout": textwrap.dedent(
        """\
        name template_4x2_X8
        version 1.0
        target X8_01 (shots=1)

        # for n spatial degrees, first n signal modes, then n idler modes, all phases zero
        S2gate({squeezing_amplitude_0}, 0.0) | [0, 4]
        S2gate({squeezing_amplitude_1}, 0.0) | [1, 5]
        S2gate({squeezing_amplitude_2}, 0.0) | [2, 6]
        S2gate({squeezing_amplitude_3}, 0.0) | [3, 7]

        # standard 4x4 interferometer for the signal modes (the lower ones in frequency)
        # even phase indices correspond to internal Mach-Zehnder interferometer phases
        # odd phase indices correspond to external Mach-Zehnder interferometer phases
        MZgate({phase_0}, {phase_1}) | [0, 1]
        MZgate({phase_2}, {phase_3}) | [2, 3]
        MZgate({phase_4}, {phase_5}) | [1, 2]
        MZgate({phase_6}, {phase_7}) | [0, 1]
        MZgate({phase_8}, {phase_9}) | [2, 3]
        MZgate({phase_10}, {phase_11}) | [1, 2]

        # duplicate the interferometer for the idler modes (the higher ones in frequency)
        MZgate({phase_0}, {phase_1}) | [4, 5]
        MZgate({phase_2}, {phase_3}) | [6, 7]
        MZgate({phase_4}, {phase_5}) | [5, 6]
        MZgate({phase_6}, {phase_7}) | [4, 5]
        MZgate({phase_8}, {phase_9}) | [6, 7]
        MZgate({phase_10}, {phase_11}) | [5, 6]

        # add final dummy phases to allow mapping any unitary to this template (these do not
        # affect the photon number measurement)
        Rgate({final_phase_0}) | [0]
        Rgate({final_phase_1}) | [1]
        Rgate({final_phase_2}) | [2]
        Rgate({final_phase_3}) | [3]
        Rgate({final_phase_4}) | [4]
        Rgate({final_phase_5}) | [5]
        Rgate({final_phase_6}) | [6]
        Rgate({final_phase_7}) | [7]

        # measurement in Fock basis
        MeasureFock() | [0, 1, 2, 3, 4, 5, 6, 7]
    """
    ),
    "modes": 8,
    "compiler": [],
    "gate_parameters": {
        "squeezing_amplitude_0": [0, 1],
        "squeezing_amplitude_1": [0, 1],
        "squeezing_amplitude_2": [0, 1],
        "squeezing_amplitude_3": [0, 1],
        "phase_0": [0, [0, 6.283185307179586]],
        "phase_1": [0, [0, 6.283185307179586]],
        "phase_2": [0, [0, 6.283185307179586]],
        "phase_3": [0, [0, 6.283185307179586]],
        "phase_4": [0, [0, 6.283185307179586]],
        "phase_5": [0, [0, 6.283185307179586]],
        "phase_6": [0, [0, 6.283185307179586]],
        "phase_7": [0, [0, 6.283185307179586]],
        "phase_8": [0, [0, 6.283185307179586]],
        "phase_9": [0, [0, 6.283185307179586]],
        "phase_10": [0, [0, 6.283185307179586]],
        "phase_11": [0, [0, 6.283185307179586]],
        "final_phase_0": [0, [0, 6.283185307179586]],
        "final_phase_1": [0, [0, 6.283185307179586]],
        "final_phase_2": [0, [0, 6.283185307179586]],
        "final_phase_3": [0, [0, 6.283185307179586]],
        "final_phase_4": [0, [0, 6.283185307179586]],
        "final_phase_5": [0, [0, 6.283185307179586]],
        "final_phase_6": [0, [0, 6.283185307179586]],
        "final_phase_7": [0, [0, 6.283185307179586]],
    },
}

def generate_X8_params(r, p):
    return {
        "squeezing_amplitude_0": r,
        "squeezing_amplitude_1": r,
        "squeezing_amplitude_2": r,
        "squeezing_amplitude_3": r,
        "phase_0": p,
        "phase_1": p,
        "phase_2": p,
        "phase_3": p,
        "phase_4": p,
        "phase_5": p,
        "phase_6": p,
        "phase_7": p,
        "phase_8": p,
        "phase_9": p,
        "phase_10": p,
        "phase_11": p,
        "final_phase_0": 1.24,
        "final_phase_1": 0.54,
        "final_phase_2": 4.12,
        "final_phase_3": 0,
        "final_phase_4": 1.24,
        "final_phase_5": 0.54,
        "final_phase_6": 4.12,
        "final_phase_7": 0,
    }

X8_spec = DeviceSpec(target="X8", connection=None, spec=test_spec) # <--- specifying X8 as target instead of X8_01

params = generate_X8_params(1, 0.3)

prog = X8_spec.create_program(**params)

eng = sf.RemoteEngine("X8_01")
device = eng.device_spec

res = eng.run(prog, shots=10)

Additional information

The solution could involve removing the target argument and deriving target from the layout specified. This might be a breaking change to the stack though.

josh146 commented 4 years ago

Perhaps a short term solution is to simply include validation on Spec creation, to ensure that the two target values are identical?

thisac commented 2 years ago

This has been fixed in #656. The DeviceSpec object uses the target value in the device specification dictionary. There could still potentially be two targets in the specification dictionary (in the layout and in the target field), although this is related to the device specifications on the platform and not an SF issue.