From c01ec38e23be569068ab0f4481c6be1a61240dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Kolarovszki?= Date: Wed, 14 Feb 2024 00:06:01 +0100 Subject: [PATCH] feat(cvqnn): CVQNN module A module has been written to support CVQNN-related computations. `tf_cvnn_benchmark.py` has been rewritten using this module. --- benchmarks/tf_cvnn_benchmark.py | 112 +++++----------- docs/index.rst | 7 + docs/misc/cvqnn.rst | 8 ++ piquasso/api/exceptions.py | 4 + piquasso/cvqnn.py | 226 ++++++++++++++++++++++++++++++++ tests/test_cvqnn.py | 168 ++++++++++++++++++++++++ 6 files changed, 444 insertions(+), 81 deletions(-) create mode 100644 docs/misc/cvqnn.rst create mode 100644 piquasso/cvqnn.py create mode 100644 tests/test_cvqnn.py diff --git a/benchmarks/tf_cvnn_benchmark.py b/benchmarks/tf_cvnn_benchmark.py index 9f95a0c9..ee4f61eb 100644 --- a/benchmarks/tf_cvnn_benchmark.py +++ b/benchmarks/tf_cvnn_benchmark.py @@ -21,15 +21,20 @@ import pytest import numpy as np -import tensorflow as tf +import tensorflow as tf import strawberryfields as sf + import piquasso as pq +from piquasso import cvqnn + + +np.set_printoptions(suppress=True, linewidth=200) @pytest.fixture def cutoff(): - return 10 + return 20 @pytest.fixture @@ -44,7 +49,7 @@ def d(): @pytest.fixture def weights(layer_count, d): - active_sd = 0.0001 + active_sd = 0.01 passive_sd = 0.1 M = int(d * (d - 1)) + max(1, d - 1) @@ -69,104 +74,49 @@ def weights(layer_count, d): return weights -def piquasso_benchmark(benchmark, weights, d, cutoff, layer_count): - benchmark(lambda: _calculate_piquasso_results(weights, d, cutoff, layer_count)) +def piquasso_benchmark(benchmark, weights, cutoff): + benchmark(lambda: _calculate_piquasso_results(weights, cutoff)) -def strawberryfields_benchmark(benchmark, weights, d, cutoff, layer_count): - benchmark( - lambda: _calculate_strawberryfields_results(weights, d, cutoff, layer_count) - ) +def strawberryfields_benchmark(benchmark, weights, cutoff): + benchmark(lambda: _calculate_strawberryfields_results(weights, cutoff)) -def test_state_vector_and_jacobian(weights, d, cutoff, layer_count): - pq_state_vector, pq_jacobian = _calculate_piquasso_results( - weights, d, cutoff, layer_count - ) - sf_state_vector, sf_jacobian = _calculate_strawberryfields_results( - weights, d, cutoff, layer_count - ) +def test_state_vector_and_jacobian(weights, cutoff): + pq_state_vector, pq_jacobian = _calculate_piquasso_results(weights, cutoff) + sf_state_vector, sf_jacobian = _calculate_strawberryfields_results(weights, cutoff) - assert np.allclose(pq_state_vector, sf_state_vector) - assert np.allclose(pq_jacobian, sf_jacobian) + assert np.sum(np.abs(pq_state_vector - sf_state_vector) ** 2) < 1e-10 + assert np.sum(np.abs(pq_jacobian - sf_jacobian) ** 2) < 1e-10 -def _calculate_piquasso_results(weights, d, cutoff, layer_count): +def _pq_state_vector(weights, cutoff): + d = cvqnn.get_number_of_modes(weights.shape[1]) + simulator = pq.PureFockSimulator( d=d, - config=pq.Config(cutoff=cutoff, dtype=weights.dtype, normalize=False), + config=pq.Config(cutoff=cutoff, normalize=False), calculator=pq.TensorflowCalculator(), ) - with tf.GradientTape() as tape: - instructions = [] + program = cvqnn.create_program(weights) - for i in range(layer_count): - instructions.extend(_pq_layer(weights[i], d)) + state = simulator.execute(program).state - program = pq.Program(instructions=[pq.Vacuum()] + instructions) - - state = simulator.execute(program).state - state_vector = state.get_tensor_representation() - - return state_vector, tape.jacobian(state_vector, weights) - - -def _pq_interferometer(params, d): - instructions = [] - - theta = params[: d * (d - 1) // 2] - phi = params[d * (d - 1) // 2 : d * (d - 1)] - rphi = params[-d + 1 :] - - if d == 1: - return [pq.Phaseshifter(rphi[0]).on_modes(0)] - - n = 0 + return state.get_tensor_representation() - q = tuple(range(d)) - for j in range(d): - for k, (q1, q2) in enumerate(zip(q[:-1], q[1:])): - if (j + k) % 2 != 1: - instructions.append( - pq.Beamsplitter(theta=theta[n], phi=phi[n]).on_modes(q1, q2) - ) - n += 1 - - instructions.extend( - [pq.Phaseshifter(rphi[i]).on_modes(i) for i in range(max(1, d - 1))] - ) - - return instructions - - -def _pq_layer(params, d): - M = int(d * (d - 1)) + max(1, d - 1) - - int1 = params[:M] - s = params[M : M + d] - int2 = params[M + d : 2 * M + d] - dr = params[2 * M + d : 2 * M + 2 * d] - dp = params[2 * M + 2 * d : 2 * M + 3 * d] - k = params[2 * M + 3 * d : 2 * M + 4 * d] - - instructions = [] - - instructions.extend(_pq_interferometer(int1, d)) - - instructions.extend([pq.Squeezing(s[i]).on_modes(i) for i in range(d)]) - - instructions.extend(_pq_interferometer(int2, d)) - - instructions.extend([pq.Displacement(dr[i], dp[i]).on_modes(i) for i in range(d)]) +def _calculate_piquasso_results(weights, cutoff): + with tf.GradientTape() as tape: + state_vector = _pq_state_vector(weights, cutoff) - instructions.extend([pq.Kerr(k[i]).on_modes(i) for i in range(d)]) + return state_vector, tape.jacobian(state_vector, weights) - return instructions +def _calculate_strawberryfields_results(weights, cutoff): + layer_count = weights.shape[0] + d = cvqnn.get_number_of_modes(weights.shape[1]) -def _calculate_strawberryfields_results(weights, d, cutoff, layer_count): eng = sf.Engine(backend="tf", backend_options={"cutoff_dim": cutoff}) qnn = sf.Program(d) diff --git a/docs/index.rst b/docs/index.rst index d294a7e8..22a87731 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -70,3 +70,10 @@ One could easily install Piquasso with the following command: api/instruction api/result api/exceptions + +.. toctree:: + :maxdepth: 3 + :caption: Miscellaneous: + :hidden: + + misc/cvqnn \ No newline at end of file diff --git a/docs/misc/cvqnn.rst b/docs/misc/cvqnn.rst new file mode 100644 index 00000000..fca1ff35 --- /dev/null +++ b/docs/misc/cvqnn.rst @@ -0,0 +1,8 @@ +CVQNN Module +============ + +Some tools for simulating CVQNN (Continuous-Variable Quantum Neural Networks) are also +available in Piquasso. + +.. automodule:: piquasso.cvqnn + :members: diff --git a/piquasso/api/exceptions.py b/piquasso/api/exceptions.py index aea61f02..e92480eb 100644 --- a/piquasso/api/exceptions.py +++ b/piquasso/api/exceptions.py @@ -44,3 +44,7 @@ class InvalidSimulation(PiquassoException): class NotImplementedCalculation(PiquassoException): """Raiesed when a calculation is not implemented.""" + + +class CVQNNException(PiquassoException): + """Exception raised from the `cvqnn` module.""" diff --git a/piquasso/cvqnn.py b/piquasso/cvqnn.py new file mode 100644 index 00000000..37d59722 --- /dev/null +++ b/piquasso/cvqnn.py @@ -0,0 +1,226 @@ +# +# Copyright 2021-2024 Budapest Quantum Computing Group +# +# 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. + +from typing import List + +import numpy as np + +from dataclasses import dataclass + +from piquasso import ( + Program, + Instruction, + Vacuum, + Phaseshifter, + Beamsplitter, + Squeezing, + Displacement, + Kerr, +) + +from piquasso.api.exceptions import CVQNNException + + +@dataclass +class _InterferometerParameters: + thetas: np.ndarray + phis: np.ndarray + final_phis: np.ndarray + + +@dataclass +class _LayerParameters: + first_interferometer: _InterferometerParameters + squeezing_parameters: np.ndarray + second_interferometer: _InterferometerParameters + displacement_amplitudes: np.ndarray + displacement_angles: np.ndarray + kerr_parameters: np.ndarray + + +def generate_random_cvqnn_weights( + layer_count: int, d: int, active_var: float = 0.01, passive_var: float = 0.1 +) -> np.ndarray: + """Generates random CVQNN weights for :func:`create_program`. + + Args: + layer_count (int): The number of layers. + d (int): Number of modes. + active_var (float, optional): Active gate variance. Defaults to 0.01. + passive_var (float, optional): Passive gate variance. Defaults to 0.1. + + Returns: + np.ndarray: An array of weights. + """ + M = _get_number_of_interferometer_parameters(d) + + first_interferometer = np.random.normal(size=[layer_count, M], scale=passive_var) + squeezing_parameters = np.random.normal(size=[layer_count, d], scale=active_var) + second_interferometer = np.random.normal(size=[layer_count, M], scale=passive_var) + displacement_amplitudes = np.random.normal(size=[layer_count, d], scale=active_var) + displacement_angles = np.random.normal(size=[layer_count, d], scale=passive_var) + kerr_parameters = np.random.normal(size=[layer_count, d], scale=active_var) + + return np.concatenate( + [ + first_interferometer, + squeezing_parameters, + second_interferometer, + displacement_amplitudes, + displacement_angles, + kerr_parameters, + ], + axis=1, + ) + + +def create_program(weights: np.ndarray) -> Program: + """Creates a `Program` from the specified weights. + + Args: + weights (np.ndarray): The CVQNN circuit weights. + + Returns: + Program: The program of the CVQNN circuit, ready to be executed. + """ + d = get_number_of_modes(weights.shape[1]) + layer_count = weights.shape[0] + + instructions = [] + + for k in range(layer_count): + layer_parameters = _parse_layer(weights[k], d) + instructions.extend(_instructions_from_layer(layer_parameters, d)) + + return Program([Vacuum()] + instructions) + + +def get_number_of_modes(number_of_parameters: int) -> int: + """Returns the number of modes given the number of parameters in a CVQNN layer. + + Args: + number_of_parameters (int): The number of CVQNN parameters per layer. + + Raises: + CVQNNException: + If the number of parameters does not correspond to any number of modes + + Returns: + int: The number of modes. + """ + if number_of_parameters == 6: + return 1 + + def func_to_invert(d): + return 2 * (d**2 + 2 * d - 1) + + d = 2 + + while func_to_invert(d) < number_of_parameters: + d += 1 + + if not func_to_invert(d) == number_of_parameters: + raise CVQNNException( + f"Invalid number of parameters specified: '{number_of_parameters}'." + ) + + return d + + +def _get_number_of_interferometer_parameters(d): + return int(d * (d - 1)) + max(1, d - 1) + + +def _parse_interferometer(weights: np.ndarray, d: int) -> _InterferometerParameters: + return _InterferometerParameters( + thetas=weights[: d * (d - 1) // 2], + phis=weights[d * (d - 1) // 2 : d * (d - 1)], + final_phis=weights[-d + 1 :], + ) + + +def _parse_layer(weights: np.ndarray, d: int) -> _LayerParameters: + M = _get_number_of_interferometer_parameters(d) + + return _LayerParameters( + _parse_interferometer(weights[:M], d), + weights[M : M + d], + _parse_interferometer(weights[M + d : 2 * M + d], d), + weights[2 * M + d : 2 * M + 2 * d], + weights[2 * M + 2 * d : 2 * M + 3 * d], + weights[2 * M + 3 * d : 2 * M + 4 * d], + ) + + +def _instructions_from_interferometer( + interferometer_parameters: _InterferometerParameters, d: int +) -> List[Instruction]: + instructions = [] + + thetas = interferometer_parameters.thetas + phis = interferometer_parameters.phis + final_phis = interferometer_parameters.final_phis + + if d == 1: + return [Phaseshifter(final_phis[0]).on_modes(0)] + + n = 0 + indices = [list(range(0, d - 1, 2)), list(range(1, d - 1, 2))] + + for j in range(d): + for k in indices[j % 2]: + instructions.append( + Beamsplitter(theta=thetas[n], phi=phis[n]).on_modes(k, k + 1) + ) + n += 1 + + instructions.extend( + [Phaseshifter(final_phis[i]).on_modes(i) for i in range(max(1, d - 1))] + ) + + return instructions + + +def _instructions_from_layer( + layer_parameters: _LayerParameters, d: int +) -> List[Instruction]: + instructions = [] + + instructions.extend( + _instructions_from_interferometer(layer_parameters.first_interferometer, d) + ) + + squeezing_parameters = layer_parameters.squeezing_parameters + instructions.extend( + [Squeezing(squeezing_parameters[i]).on_modes(i) for i in range(d)] + ) + + instructions.extend( + _instructions_from_interferometer(layer_parameters.second_interferometer, d) + ) + + displacement_amplitudes = layer_parameters.displacement_amplitudes + displacement_angles = layer_parameters.displacement_angles + instructions.extend( + [ + Displacement(displacement_amplitudes[i], displacement_angles[i]).on_modes(i) + for i in range(d) + ] + ) + + kerr_parameters = layer_parameters.kerr_parameters + instructions.extend([Kerr(kerr_parameters[i]).on_modes(i) for i in range(d)]) + + return instructions diff --git a/tests/test_cvqnn.py b/tests/test_cvqnn.py new file mode 100644 index 00000000..ed4ef4a7 --- /dev/null +++ b/tests/test_cvqnn.py @@ -0,0 +1,168 @@ +# +# Copyright 2021-2024 Budapest Quantum Computing Group +# +# 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. + +import pytest + +import numpy as np + +import piquasso as pq + +from piquasso import cvqnn + + +@pytest.mark.parametrize("layer_count, d", [(1, 2), (2, 1), (2, 2), (3, 4), (5, 10)]) +def test_generate_random_cvqnn_weights(layer_count, d): + weights = cvqnn.generate_random_cvqnn_weights( + layer_count=layer_count, d=d, active_var=0.1, passive_var=0.1 + ) + + assert weights.shape[0] == layer_count + assert weights.shape[1] == 4 * d + 2 * (d * (d - 1) + max(1, d - 1)) + + +def test_create_program_yields_valid_program(): + d = 3 + + weights = cvqnn.generate_random_cvqnn_weights(layer_count=10, d=d) + program = cvqnn.create_program(weights) + + simulator = pq.PureFockSimulator(d) + + state = simulator.execute(program).state + + state.validate() + + +def test_create_program_yields_valid_program_for_1_mode(): + d = 1 + + weights = cvqnn.generate_random_cvqnn_weights(layer_count=10, d=d) + program = cvqnn.create_program(weights) + + simulator = pq.PureFockSimulator(d) + + state = simulator.execute(program).state + + state.validate() + + +def test_create_program_state_vector(): + d = 2 + + weights = np.array( + [ + [ + 0.12856391, + -0.03381019, + -0.1377453, + -0.00976438, + -0.00987302, + -0.03386338, + -0.08224322, + 0.14441779, + -0.00468172, + -0.00120696, + -0.04211308, + -0.19803922, + -0.0032648, + 0.01448497, + ], + [ + 0.05684676, + 0.04780328, + -0.0244914, + 0.01133317, + 0.00669194, + 0.02037788, + 0.03323016, + 0.13712651, + -0.01981095, + -0.00937427, + 0.00490652, + -0.16186161, + 0.01149154, + 0.01250349, + ], + [ + 0.14877177, + 0.04202048, + 0.16624834, + 0.00349866, + -0.01133043, + 0.19474302, + -0.01333464, + -0.08560477, + -0.00291201, + -0.01066722, + 0.03029708, + -0.06975938, + 0.00673455, + -0.00111852, + ], + ] + ) + + program = cvqnn.create_program(weights) + + simulator = pq.PureFockSimulator(d) + + state = simulator.execute(program).state + + assert np.allclose( + state._state_vector, + [ + 0.99927124 - 0.00000994j, + -0.02215289 - 0.00339542j, + -0.02876671 + 0.00127763j, + -0.00304377 + 0.0010067j, + -0.00414854 + 0.0005445j, + 0.00985761 + 0.00054058j, + 0.00013097 - 0.00000967j, + 0.00023877 - 0.00002112j, + -0.00002112 - 0.00007153j, + -0.00047556 - 0.00000858j, + ], + ) + + +def test_create_program_with_invalid_weights(): + weights = np.empty(shape=(3, 7)) + + with pytest.raises(pq.api.exceptions.CVQNNException) as error: + cvqnn.create_program(weights) + + assert ( + error.value.args[0] + == f"Invalid number of parameters specified: '{weights.shape[1]}'." + ) + + +@pytest.mark.parametrize( + "number_of_parameters, number_of_modes", [(6, 1), (14, 2), (28, 3), (46, 4)] +) +def test_get_number_of_modes(number_of_parameters, number_of_modes): + assert cvqnn.get_number_of_modes(number_of_parameters) == number_of_modes + + +def test_get_number_of_modes_with_invalid_number_of_parameters(): + invalid_number_of_parameters = 7 + + with pytest.raises(pq.api.exceptions.CVQNNException) as error: + cvqnn.get_number_of_modes(invalid_number_of_parameters) + + assert ( + error.value.args[0] + == f"Invalid number of parameters specified: '{invalid_number_of_parameters}'." + )