def test_str_repr(self, get_circuit, output_dim): """Test the __str__ and __repr__ representations""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) assert layer.__str__() == "<Quantum Keras Layer: func=circuit>" assert layer.__repr__() == "<Quantum Keras Layer: func=circuit>"
def test_qnode_weights_with_spec(self, get_circuit, monkeypatch, output_dim, n_qubits): """Test if the build() method correctly passes on user specified weight_specs to the inherited add_weight() method. This is done by monkeypatching add_weight() so that it simply returns its input keyword arguments. The qnode_weights dictionary should then have values that are the input keyword arguments, and we check that the specified weight_specs keywords are there.""" def add_weight_dummy(*args, **kwargs): """Dummy function for mocking out the add_weight method to simply return the input keyword arguments""" return kwargs weight_specs = { "w1": {"initializer": "random_uniform", "trainable": False}, "w2": {"initializer": tf.keras.initializers.RandomNormal(mean=0, stddev=0.5)}, "w3": {}, "w4": {}, "w5": {}, "w6": {}, "w7": {}, } with monkeypatch.context() as m: m.setattr(tf.keras.layers.Layer, "add_weight", add_weight_dummy) c, w = get_circuit layer = KerasLayer(c, w, output_dim, weight_specs=weight_specs) layer.build(input_shape=(10, n_qubits)) for weight in layer.weight_shapes: assert all( item in layer.qnode_weights[weight].items() for item in weight_specs[weight].items() )
def test_compute_output_shape(self, get_circuit, output_dim, input_shape): """Test if the compute_output_shape() method performs correctly, i.e., that it replaces the last element in the input_shape tuple with the specified output_dim and that the output shape is of type tf.TensorShape""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) assert layer.compute_output_shape(input_shape) == (input_shape[0], output_dim) assert isinstance(layer.compute_output_shape(input_shape), tf.TensorShape)
def test_qnode_weights(self, get_circuit, n_qubits, output_dim): """Test if the build() method correctly initializes the weights in the qnode_weights dictionary, i.e., that each value of the dictionary has correct shape and name.""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) layer.build(input_shape=(10, n_qubits)) for weight, shape in layer.weight_shapes.items(): assert layer.qnode_weights[weight].shape == shape assert layer.qnode_weights[weight].name[:-2] == weight
def test_compute_output_shape(self, get_circuit, output_dim): """Test that the compute_output_shape method returns the expected shape""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) inputs = tf.keras.Input(shape=(2, )) inputs_shape = inputs.shape output_shape = layer.compute_output_shape(inputs_shape) assert output_shape.as_list() == [None, 1]
def test_weight_shape_unspecified(self, get_circuit, output_dim): """Test if a ValueError is raised when instantiated with a weight missing from the weight_shapes dictionary""" c, w = get_circuit del w["w1"] with pytest.raises(ValueError, match="Must specify a shape for every non-input parameter"): KerasLayer(c, w, output_dim)
def test_interface_conversion(get_circuit, output_dim, skip_if_no_torch_support): """Test if input QNodes with all types of interface are converted internally to the TensorFlow interface""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) assert layer.qnode.interface == "tf"
def test_no_input(self, get_circuit, output_dim): """Test if a TypeError is raised when instantiated with a QNode that does not have an argument with name equal to the input_arg class attribute of KerasLayer""" c, w = get_circuit del c.func.sig[qml.qnn.keras.KerasLayer._input_arg] with pytest.raises(TypeError, match="QNode must include an argument with name"): KerasLayer(c, w, output_dim)
def test_call_default_input(self, get_circuit, output_dim, batch_size, n_qubits): """Test if the call() method performs correctly when the inputs argument is a default argument, i.e., that it outputs with shape (batch_size, output_dim) with results that agree with directly calling the QNode""" c, w = get_circuit @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf") def c_default(w1, w2, w3, w4, w5, w6, w7, inputs=None): """Version of the circuit with inputs as a default argument""" qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) qml.RX(w2[0], wires=0) qml.RX(w3, wires=0) qml.Rot(*w4, wires=0) qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits))) qml.Rot(*w6, wires=0) qml.RX(w7, wires=0) return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] layer = KerasLayer(c_default, w, output_dim) x = tf.ones((batch_size, n_qubits)) layer_out = layer(x) weights = [w.numpy() for w in layer.qnode_weights.values()] assert layer_out.shape == (batch_size, output_dim) assert np.allclose(layer_out[0], c(x[0], *weights))
def model(get_circuit, n_qubits, output_dim): """Fixture for creating a hybrid Keras model. The model is composed of KerasLayers sandwiched between Dense layers.""" c, w = get_circuit layer1 = KerasLayer(c, w, output_dim) layer2 = KerasLayer(c, w, output_dim) model = tf.keras.models.Sequential([ tf.keras.layers.Dense(n_qubits), layer1, tf.keras.layers.Dense(n_qubits), layer2, tf.keras.layers.Dense(output_dim), ]) return model
def test_bad_tf_version(self, get_circuit, output_dim, monkeypatch): """Test if an ImportError is raised when instantiated with an incorrect version of TensorFlow""" c, w = get_circuit with monkeypatch.context() as m: m.setattr(qml.qnn.keras, "CORRECT_TF_VERSION", False) with pytest.raises(ImportError, match="KerasLayer requires TensorFlow version 2"): KerasLayer(c, w, output_dim)
def test_deprecation_warning(self, get_circuit, output_dim): """Test if deprecation warning is raised""" if int(qml.__version__.split(".")[1]) >= 16: pytest.fail( "Deprecation warnings for the qnn module should be removed") c, w = get_circuit with pytest.warns(DeprecationWarning, match=WARNING_STRING): KerasLayer(c, w, output_dim)
def test_call(self, get_circuit, output_dim, batch_size, n_qubits): """Test if the call() method performs correctly, i.e., that it outputs with shape (batch_size, output_dim) with results that agree with directly calling the QNode""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) x = tf.ones((batch_size, n_qubits)) layer_out = layer(x) weights = [w.numpy() for w in layer.qnode_weights.values()] assert layer_out.shape == (batch_size, output_dim) assert np.allclose(layer_out[0], c(x[0], *weights))
def model_dm(get_circuit_dm, n_qubits, output_dim): c, w = get_circuit_dm layer1 = KerasLayer(c, w, output_dim) layer2 = KerasLayer(c, w, output_dim) model = tf.keras.models.Sequential([ tf.keras.layers.Dense(n_qubits), layer1, # Adding a lambda layer to take only the real values from density matrix tf.keras.layers.Lambda(lambda x: tf.abs(x)), tf.keras.layers.Flatten(), tf.keras.layers.Dense(n_qubits), layer2, # Adding a lambda layer to take only the real values from density matrix tf.keras.layers.Lambda(lambda x: tf.abs(x)), tf.keras.layers.Flatten(), tf.keras.layers.Dense(output_dim[0] * output_dim[1]) ]) return model
def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): """Test if a ValueError is raised when instantiated with a weight_shapes dictionary that contains the shape of the input argument given by the input_arg class attribute of KerasLayer""" c, w = get_circuit w[qml.qnn.keras.KerasLayer._input_arg] = n_qubits with pytest.raises( ValueError, match="{} argument should not have its dimension".format( qml.qnn.keras.KerasLayer._input_arg), ): KerasLayer(c, w, output_dim)
def test_no_input(self): """Test if a TypeError is raised when instantiated with a QNode that does not have an argument with name equal to the input_arg class attribute of KerasLayer""" dev = qml.device("default.qubit", wires=1) weight_shapes = {"w1": (3, 3), "w2": 1} @qml.qnode(dev, interface="tf") def circuit(w1, w2): return qml.expval(qml.PauliZ(0)) with pytest.raises(TypeError, match="QNode must include an argument with name"): KerasLayer(circuit, weight_shapes, output_dim=1)
def test_var_pos(self): """Test if a TypeError is raised when instantiated with a variable number of positional arguments""" dev = qml.device("default.qubit", wires=1) weight_shapes = {"w1": (3, 3), "w2": 1} @qml.qnode(dev, interface="tf") def circuit(inputs, w1, w2, *args): return qml.expval(qml.PauliZ(0)) with pytest.raises(TypeError, match="Cannot have a variable number of positional"): KerasLayer(circuit, weight_shapes, output_dim=1)
def test_weight_shapes(self, get_circuit, output_dim, n_qubits): """Test if the weight_shapes input argument is correctly processed to be a dictionary with values that are tuples.""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) assert layer.weight_shapes == { "w1": (3, n_qubits, 3), "w2": (1, ), "w3": (), "w4": (3, ), "w5": (2, n_qubits, 3), "w6": (3, ), "w7": (), }
def test_non_input_defaults(self, get_circuit, output_dim, n_qubits): """Test if a TypeError is raised when default arguments that are not the input argument are present in the QNode""" c, w = get_circuit @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf") def c_dummy(inputs, w1, w2, w3, w4, w5, w6, w7, w8=None): """Dummy version of the circuit with a default argument""" return c(inputs, w1, w2, w3, w4, w5, w6, w7) with pytest.raises( TypeError, match="Only the argument {} is permitted".format(qml.qnn.keras.KerasLayer._input_arg), ): KerasLayer(c_dummy, {**w, **{"w8": 1}}, output_dim)
def test_non_input_defaults_tape_mode(self): """Test that everything works when default arguments that are not the input argument are present in the QNode in tape mode""" if not qml.tape_mode_active(): pytest.skip("This functionality is only supported in tape mode.") n_qubits = 2 output_dim = 2 dev = qml.device("default.qubit", wires=n_qubits) w = { "w1": (3, n_qubits, 3), "w2": (1, ), "w3": 1, "w4": [3], "w5": (2, n_qubits, 3), "w6": 3, "w7": 0, } @qml.qnode(dev, interface="tf") def c(inputs, w1, w2, w4, w5, w6, w7, w3=0.5): """A circuit that embeds data using the AngleEmbedding and then performs a variety of operations. The output is a PauliZ measurement on the first output_dim qubits. One set of parameters, w5, are specified as non-trainable.""" qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) qml.RX(w2[0], wires=0 % n_qubits) qml.RX(w3, wires=1 % n_qubits) qml.Rot(*w4, wires=2 % n_qubits) qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits))) qml.Rot(*w6, wires=3 % n_qubits) qml.RX(w7, wires=4 % n_qubits) return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] layer = KerasLayer(c, w, output_dim=output_dim) x = tf.ones((2, n_qubits)) layer_out = layer(x) circ_weights = layer.qnode_weights.copy() circ_weights["w4"] = tf.convert_to_tensor( circ_weights["w4"]) # To allow for slicing circ_weights["w6"] = tf.convert_to_tensor(circ_weights["w6"]) circuit_out = c(x[0], **circ_weights) assert np.allclose(layer_out, circuit_out)
def test_var_keyword(self, get_circuit, monkeypatch, output_dim): """Test if a TypeError is raised when instantiated with a variable number of keyword arguments""" c, w = get_circuit class FuncPatch: """Patch for variable number of keyword arguments""" sig = c.func.sig var_pos = False var_keyword = True with monkeypatch.context() as m: m.setattr(c, "func", FuncPatch) with pytest.raises(TypeError, match="Cannot have a variable number of keyword"): KerasLayer(c, w, output_dim)
def test_call_broadcast(self, get_circuit, output_dim, middle_dim, batch_size, n_qubits): """Test if the call() method performs correctly when the inputs argument has an arbitrary shape (that can correctly be broadcast over), i.e., for input of shape (batch_size, dn, ... , d0) it outputs with shape (batch_size, dn, ... , d1, output_dim). Also tests if gradients are still backpropagated correctly.""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) x = tf.ones((batch_size, middle_dim, n_qubits)) with tf.GradientTape() as tape: layer_out = layer(x) g_layer = tape.gradient(layer_out, layer.trainable_variables) # test gradients are at least calculated assert g_layer is not None assert layer_out.shape == (batch_size, middle_dim, output_dim)
def test_var_keyword(self): """Test that variable number of keyword arguments works""" n_qubits = 2 output_dim = 2 dev = qml.device("default.qubit", wires=n_qubits) w = { "w1": (3, n_qubits, 3), "w2": (1, ), "w3": 1, "w4": [3], "w5": (2, n_qubits, 3), "w6": 3, "w7": 0, } @qml.qnode(dev, interface="tf") def c(inputs, **kwargs): """A circuit that embeds data using the AngleEmbedding and then performs a variety of operations. The output is a PauliZ measurement on the first output_dim qubits. One set of parameters, w5, are specified as non-trainable.""" qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) qml.templates.StronglyEntanglingLayers(kwargs["w1"], wires=list(range(n_qubits))) qml.RX(kwargs["w2"][0], wires=0 % n_qubits) qml.RX(kwargs["w3"], wires=1 % n_qubits) qml.Rot(*kwargs["w4"], wires=2 % n_qubits) qml.templates.StronglyEntanglingLayers(kwargs["w5"], wires=list(range(n_qubits))) qml.Rot(*kwargs["w6"], wires=3 % n_qubits) qml.RX(kwargs["w7"], wires=4 % n_qubits) return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] layer = KerasLayer(c, w, output_dim=output_dim) x = tf.ones((2, n_qubits)) layer_out = layer(x) circ_weights = layer.qnode_weights.copy() circ_weights["w4"] = tf.convert_to_tensor( circ_weights["w4"]) # To allow for slicing circ_weights["w6"] = tf.convert_to_tensor(circ_weights["w6"]) circuit_out = c(x[0], **circ_weights) assert np.allclose(layer_out, circuit_out)
def test_gradients(self, get_circuit, output_dim, n_qubits): """Test if the gradients of the KerasLayer are equal to the gradients of the circuit when taken with respect to the trainable variables""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) x = tf.ones((1, n_qubits)) with tf.GradientTape() as tape: out_layer = layer(x) g_layer = tape.gradient(out_layer, layer.trainable_variables) with tf.GradientTape() as tape: out_circuit = c(x[0], *layer.trainable_variables) g_circuit = tape.gradient(out_circuit, layer.trainable_variables) for i in range(len(out_layer)): assert np.allclose(g_layer[i], g_circuit[i])
def test_var_keyword(self, get_circuit, monkeypatch, output_dim): """Test if a TypeError is raised when instantiated with a variable number of keyword arguments""" if qml.tape_mode_active(): pytest.skip( "This functionality is supported in tape mode, so will not raise an exception." ) c, w = get_circuit class FuncPatch: """Patch for variable number of keyword arguments""" sig = c.func.sig var_pos = False var_keyword = True with monkeypatch.context() as m: m.setattr(c, "func", FuncPatch) with pytest.raises(TypeError, match="Cannot have a variable number of keyword"): KerasLayer(c, w, output_dim)
def test_gradients(self, get_circuit, output_dim, n_qubits): """Test if the gradients of the KerasLayer are equal to the gradients of the circuit when taken with respect to the trainable variables""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) x = tf.ones((1, n_qubits)) with tf.GradientTape() as tape: out_layer = layer(x) g_layer = tape.gradient(out_layer, layer.trainable_variables) circuit_weights = layer.trainable_variables.copy() circuit_weights[3] = tf.convert_to_tensor(circuit_weights[3]) # To allow for slicing circuit_weights[5] = tf.convert_to_tensor(circuit_weights[5]) with tf.GradientTape() as tape: out_circuit = c(x[0], *circuit_weights) g_circuit = tape.gradient(out_circuit, layer.trainable_variables) for i in range(len(out_layer)): assert np.allclose(g_layer[i], g_circuit[i])
def test_output_dim(self, get_circuit, output_dim): """Test if the output_dim is correctly processed, i.e., that an iterable is mapped to its first element while an int is left unchanged.""" c, w = get_circuit layer = KerasLayer(c, w, output_dim[0]) assert layer.output_dim == output_dim[1]