├── qinfo ├── __init__.py ├── __pycache__ │ ├── qinfo.cpython-35.pyc │ ├── qinfo.cpython-36.pyc │ ├── qinfo.cpython-37.pyc │ ├── __init__.cpython-35.pyc │ ├── __init__.cpython-36.pyc │ ├── __init__.cpython-37.pyc │ ├── operators.cpython-35.pyc │ ├── operators.cpython-36.pyc │ └── operators.cpython-37.pyc └── operators.py ├── ucell ├── __init__.py ├── __pycache__ │ ├── ucell.cpython-36.pyc │ ├── ucell.cpython-37.pyc │ ├── utility.cpython-36.pyc │ ├── utility.cpython-37.pyc │ ├── __init__.cpython-36.pyc │ ├── __init__.cpython-37.pyc │ ├── operators.cpython-36.pyc │ └── operators.cpython-37.pyc ├── operators.py ├── utility.py └── ucell.py ├── __pycache__ ├── utility.cpython-37.pyc └── test_data.cpython-37.pyc ├── README.md ├── test_data.py ├── main.py ├── encode.py └── utility.py /qinfo/__init__.py: -------------------------------------------------------------------------------- 1 | name = "qinfo" -------------------------------------------------------------------------------- /ucell/__init__.py: -------------------------------------------------------------------------------- 1 | name = "ucell" -------------------------------------------------------------------------------- /__pycache__/utility.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/__pycache__/utility.cpython-37.pyc -------------------------------------------------------------------------------- /__pycache__/test_data.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/__pycache__/test_data.cpython-37.pyc -------------------------------------------------------------------------------- /qinfo/__pycache__/qinfo.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/qinfo/__pycache__/qinfo.cpython-35.pyc -------------------------------------------------------------------------------- /qinfo/__pycache__/qinfo.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/qinfo/__pycache__/qinfo.cpython-36.pyc -------------------------------------------------------------------------------- /qinfo/__pycache__/qinfo.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/qinfo/__pycache__/qinfo.cpython-37.pyc -------------------------------------------------------------------------------- /ucell/__pycache__/ucell.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/ucell/__pycache__/ucell.cpython-36.pyc -------------------------------------------------------------------------------- /ucell/__pycache__/ucell.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/ucell/__pycache__/ucell.cpython-37.pyc -------------------------------------------------------------------------------- /ucell/__pycache__/utility.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/ucell/__pycache__/utility.cpython-36.pyc -------------------------------------------------------------------------------- /ucell/__pycache__/utility.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/ucell/__pycache__/utility.cpython-37.pyc -------------------------------------------------------------------------------- /qinfo/__pycache__/__init__.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/qinfo/__pycache__/__init__.cpython-35.pyc -------------------------------------------------------------------------------- /qinfo/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/qinfo/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /qinfo/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/qinfo/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /qinfo/__pycache__/operators.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/qinfo/__pycache__/operators.cpython-35.pyc -------------------------------------------------------------------------------- /qinfo/__pycache__/operators.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/qinfo/__pycache__/operators.cpython-36.pyc -------------------------------------------------------------------------------- /qinfo/__pycache__/operators.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/qinfo/__pycache__/operators.cpython-37.pyc -------------------------------------------------------------------------------- /ucell/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/ucell/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /ucell/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/ucell/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /ucell/__pycache__/operators.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/ucell/__pycache__/operators.cpython-36.pyc -------------------------------------------------------------------------------- /ucell/__pycache__/operators.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QCmonk/Qmemristor/HEAD/ucell/__pycache__/operators.cpython-37.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qmemristor 2 | Code base for simulating and testing a quantum neural network with optical memristors as a nonlinear element. Also has some useful quantum optical functionality. 3 | -------------------------------------------------------------------------------- /test_data.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # define binary digit arrays 4 | zero = [[1,1,1,1], 5 | [1,0,0,1], 6 | [1,0,0,1], 7 | [1,0,0,1], 8 | [1,1,1,1]] 9 | zero = np.asarray(zero) 10 | 11 | one = [[0,1,1,0], 12 | [0,0,1,0], 13 | [0,0,1,0], 14 | [0,0,1,0], 15 | [0,0,1,0]] 16 | one = np.asarray(one) 17 | 18 | two = [[0,1,1,0], 19 | [0,0,0,1], 20 | [0,0,1,0], 21 | [0,1,0,0], 22 | [1,1,1,1]] 23 | two = np.asarray(two) 24 | 25 | three = [[1,1,1,0], 26 | [0,0,0,1], 27 | [0,1,1,0], 28 | [0,0,0,1], 29 | [1,1,1,0]] 30 | three = np.asarray(three) 31 | 32 | four = [[1,0,1,0], 33 | [1,0,1,0], 34 | [1,1,1,1], 35 | [0,0,1,0], 36 | [0,0,1,0]] 37 | four = np.asarray(four) 38 | 39 | five = [[0,1,1,1], 40 | [0,1,0,0], 41 | [0,1,1,1], 42 | [0,0,0,1], 43 | [1,1,1,1]] 44 | five = np.asarray(five) 45 | 46 | six = [[1,1,1,0], 47 | [1,0,0,0], 48 | [1,1,1,1], 49 | [1,0,0,1], 50 | [1,1,1,1]] 51 | six = np.asarray(six) 52 | 53 | seven = [[1,1,1,1], 54 | [0,0,0,1], 55 | [0,0,1,0], 56 | [0,0,1,0], 57 | [0,0,1,0]] 58 | seven = np.asarray(seven) 59 | 60 | eight = [[1,1,1,1], 61 | [1,0,0,1], 62 | [1,1,1,1], 63 | [1,0,0,1], 64 | [1,1,1,1]] 65 | eight = np.asarray(eight) 66 | 67 | nine = [[1,1,1,1], 68 | [1,0,0,1], 69 | [1,1,1,1], 70 | [0,0,0,1], 71 | [0,0,0,1]] 72 | nine = np.asarray(nine) 73 | 74 | 75 | # generate data and labels 76 | data = np.asarray([zero, one, two, three, four, five, six, seven, eight, nine]) 77 | labels = [0,1,2,3,4,5,6,7,8,9] -------------------------------------------------------------------------------- /qinfo/operators.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import block_diag 3 | 4 | 5 | # Plancks constant 6 | pbar = 6.626070040e-34 7 | # reduced 8 | hbar = pbar/(2*np.pi) 9 | # Bohr magneton in J/Gauss 10 | mub = (9.274009994e-24)/1e4 11 | # g factor 12 | gm = 2.00231930436 13 | # Gyromagnetic ratio 14 | gyro = 699.9e3 15 | 16 | # identity matrix 17 | _ID = np.matrix([[1, 0], [0, 1]]) 18 | # X gate 19 | _X = np.matrix([[0, 1], [1, 0]]) 20 | # Z gate 21 | _Z = np.matrix([[1, 0], [0, -1]]) 22 | # Hadamard gate 23 | _H = (1/np.sqrt(2))*np.matrix([[1, 1], [1, -1]]) 24 | # Y Gate 25 | _Y = np.matrix([[0, -1j], [1j, 0]]) 26 | # S gate 27 | _S = np.matrix([[1, 0], [0, 1j]]) 28 | # Sdg gate 29 | _Sdg = np.matrix([[1, 0], [0, -1j]]) 30 | # T gate 31 | _T = np.matrix([[1, 0], [0, (1 + 1j)/np.sqrt(2)]]) 32 | # Tdg gate 33 | _Tdg = np.matrix([[1, 0], [0, (1 - 1j)/np.sqrt(2)]]) 34 | # CNOT gate 35 | _CX = np.matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) 36 | # CNOT inverse 37 | _CXdg = np.matrix([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]]) 38 | # SWAP gate 39 | _SWAP = np.matrix([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) 40 | # toffoli gate 41 | _TOFFOLI = block_diag(_ID, _ID, _CX) 42 | # zero state 43 | _pz = np.matrix([[1,0],[0,0]]) 44 | # one state 45 | _po = np.matrix([[0,0],[0,1]]) 46 | # entangled |psi+> state 47 | _ent = np.matrix([[1,0,0,1],[0,0,0,0],[0,0,0,0],[1,0,0,1]])/2 48 | 49 | 50 | # define operators for spin 1/2 51 | op = {'h': _H, 52 | 'id': _ID, 53 | 'x': _X, 54 | 'y': _Y, 55 | 'z': _Z, 56 | 't': _T, 57 | 'tdg': _Tdg, 58 | 's': _S, 59 | 'sdg': _Sdg, 60 | 'cx': _CX, 61 | 'cxdg': _CXdg, 62 | 'swap': _SWAP, 63 | 'toff': _TOFFOLI} 64 | 65 | states = {'pz': _pz, 66 | 'po': _po, 67 | 'ent': _ent} 68 | 69 | 70 | # measurement projections for spin 1/2 71 | meas1 = {"0":np.asarray([[1,0]]), 72 | "1":np.asarray([[0,1]]), 73 | "+":np.asarray([[1,1]]/np.sqrt(2)), 74 | "-":np.asarray([[1,-1]]/np.sqrt(2)), 75 | "+i":np.asarray([[1,1j]]/np.sqrt(2)), 76 | "-i":np.asarray([[1,-1j]]/np.sqrt(2)), 77 | } -------------------------------------------------------------------------------- /ucell/operators.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import block_diag 3 | 4 | 5 | # Plancks constant 6 | pbar = 6.626070040e-34 7 | # reduced 8 | hbar = pbar/(2*np.pi) 9 | # Bohr magneton in J/Gauss 10 | mub = (9.274009994e-24)/1e4 11 | # g factor 12 | gm = 2.00231930436 13 | # Gyromagnetic ratio 14 | gyro = 699.9e3 15 | 16 | # identity matrix 17 | _ID = np.matrix([[1, 0], [0, 1]]) 18 | # X gate 19 | _X = np.matrix([[0, 1], [1, 0]]) 20 | # Z gate 21 | _Z = np.matrix([[1, 0], [0, -1]]) 22 | # Hadamard gate 23 | _H = (1/np.sqrt(2))*np.matrix([[1, 1], [1, -1]]) 24 | # Y Gate 25 | _Y = np.matrix([[0, -1j], [1j, 0]]) 26 | # S gate 27 | _S = np.matrix([[1, 0], [0, 1j]]) 28 | # Sdg gate 29 | _Sdg = np.matrix([[1, 0], [0, -1j]]) 30 | # T gate 31 | _T = np.matrix([[1, 0], [0, (1 + 1j)/np.sqrt(2)]]) 32 | # Tdg gate 33 | _Tdg = np.matrix([[1, 0], [0, (1 - 1j)/np.sqrt(2)]]) 34 | # CNOT gate 35 | _CX = np.matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) 36 | # CNOT inverse 37 | _CXdg = np.matrix([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]]) 38 | # SWAP gate 39 | _SWAP = np.matrix([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) 40 | # toffoli gate 41 | _TOFFOLI = block_diag(_ID, _ID, _CX) 42 | # zero state 43 | _pz = np.matrix([[1,0],[0,0]]) 44 | # one state 45 | _po = np.matrix([[0,0],[0,1]]) 46 | # entangled |psi+> state 47 | _ent = np.matrix([[1,0,0,1],[0,0,0,0],[0,0,0,0],[1,0,0,1]])/2 48 | 49 | 50 | # define operators for spin 1/2 51 | op = {'h': _H, 52 | 'id': _ID, 53 | 'x': _X, 54 | 'y': _Y, 55 | 'z': _Z, 56 | 't': _T, 57 | 'tdg': _Tdg, 58 | 's': _S, 59 | 'sdg': _Sdg, 60 | 'cx': _CX, 61 | 'cxdg': _CXdg, 62 | 'swap': _SWAP, 63 | 'toff': _TOFFOLI} 64 | 65 | states = {'pz': _pz, 66 | 'po': _po, 67 | 'ent': _ent} 68 | 69 | 70 | # measurement projections for spin 1/2 71 | meas1 = {"0":np.asarray([[1,0]]), 72 | "1":np.asarray([[0,1]]), 73 | "+":np.asarray([[1,1]]/np.sqrt(2)), 74 | "-":np.asarray([[1,-1]]/np.sqrt(2)), 75 | "+i":np.asarray([[1,1j]]/np.sqrt(2)), 76 | "-i":np.asarray([[1,-1j]]/np.sqrt(2)), 77 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import os 6 | import sys 7 | import copy 8 | import time 9 | import keras 10 | import warnings 11 | import collections 12 | 13 | import numpy as np 14 | from utility import * 15 | from test_data import * 16 | import tensorflow as tf 17 | from qinfo.qinfo import partial_trace2 18 | from keras.models import Model 19 | import matplotlib.pyplot as plt 20 | from keras.layers import Input 21 | from datetime import datetime 22 | from keras import optimizers 23 | from scipy.special import comb 24 | from keras.initializers import RandomUniform, Identity 25 | from keras.layers import Dense, ReLU 26 | from keras import activations 27 | from keras.callbacks import LearningRateScheduler, TensorBoard 28 | from ucell.ucell import UParamLayer, RevDense, ReNormaliseLayer, ULayer 29 | 30 | 31 | config = tf.compat.v1.ConfigProto(intra_op_parallelism_threads=12, 32 | inter_op_parallelism_threads=12, 33 | allow_soft_placement=True, 34 | device_count = {'CPU': 24}) 35 | 36 | session = tf.compat.v1.Session(config=config) 37 | #np.random.seed(1234) 38 | 39 | 40 | # ------------------------------------------------------------ 41 | # Section 0: Program parameter definitions 42 | # ------------------------------------------------------------ 43 | # define target modes of memristors 44 | targets = [[1,2,3],[4,5,6]] 45 | # define temporal depth 46 | temporal_num = 1 47 | # define total mode number 48 | modes = 6 49 | # define total number of single photons 50 | photons = 3 51 | # number of layers in neural network 52 | neural_layers = 1 53 | # number of logits in each layer 54 | layer_units = 30 55 | # number of epochs to train over 56 | epochs = 15 57 | # initial learning rate 58 | init_lr = 5e-2 59 | # batch size for learning 60 | batch_size = 5 61 | # learning rate polynomial 62 | lr_pow = 0 63 | # whether or not to train the network or just compile it 64 | train = True 65 | # whether to save resevoir mapping channel or recompute from scratch 66 | save = True 67 | # location and name of save file 68 | file_name = "MNIST_MAP.npz"#"\\\\FREENAS\\Organon\\Research\\Papers\\QMemristor\\Code\\Archive\\MNIST_MAP.npz" 69 | # location and name of model save 70 | modelfile_name = ""#"\\\\FREENAS\\Organon\\Research\\Archive\\ML\\models\\checkpoint" 71 | # task toggle ("witness" or "mnist") 72 | task = "mnist" 73 | # ------------------------------------------------------------ 74 | # Section 1: define program constants 75 | # ------------------------------------------------------------ 76 | # define spatial depth 77 | spatial_num = len(targets) 78 | # define a single instance of memristor element as example desciption 79 | pdesc = {'theta_init':0.1,'MZI_target': [1,2,2], "tlen":10} 80 | # compute dimension of input network 81 | dim = comb(modes+photons-1, photons, exact=True) 82 | # initialiser for dense network 83 | init = RandomUniform(minval=-1, maxval=1, seed=None) 84 | # ------------------------------------------------------------ 85 | # Section 2: Apply resevoir compute layer to quantum data encoding 86 | # ------------------------------------------------------------ 87 | 88 | if task is "witness": 89 | # generate random entangled and seperable states 90 | data_train, y_train, data_test, y_test = entanglement_gen(dim=10, num=5000, partition=0.5, embed_dim=dim) 91 | 92 | data_train = reservoir_map(data_train, modes, photons, pdesc, targets, temporal_num) 93 | data_test = reservoir_map(data_test, modes, photons, pdesc, targets, temporal_num) 94 | 95 | else: 96 | 97 | # check if data has already been saved, else we will need to regenerate 98 | if os.path.isfile(file_name): 99 | # load saved data 100 | save_data = np.load(file_name) 101 | # extract all data 102 | data_train = save_data["data_train"] 103 | data_test = save_data["data_test"] 104 | y_train = save_data["y_train"] 105 | y_test = save_data["y_test"] 106 | 107 | else: 108 | 109 | # load MNIST data 110 | (x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data() 111 | 112 | x_train, y_train = filter_36(x_train, y_train) 113 | x_test, y_test = filter_36(x_test, y_test) 114 | 115 | # add channel axis (tensorflow being lame) 116 | x_train = x_train[...,tf.newaxis] 117 | x_test = x_test[...,tf.newaxis] 118 | # downsample for easy initial training 119 | data_train = np.squeeze(tf.image.resize(x_train, (14,14)).numpy())[:,1:-1,2:-2] 120 | data_test = np.squeeze(tf.image.resize(x_test, (14,14)).numpy())[:,1:-1,2:-2] 121 | #data_train[:,-1,:] = data_test[:,-1,:] = 1.0 122 | 123 | # remove conflicting training items 124 | data_train, y_train = remove_contradicting(data_train, y_train) 125 | data_train, y_train = remove_contradicting(data_test, y_test) 126 | 127 | # apply encoding of classical data to quantum state space 128 | encoder = QEncoder(modes=modes, photons=photons, density=True) 129 | #data_train = encoder.encode(data=data_train, method="amplitude", normalise=True) 130 | data_test = encoder.encode(data=data_test, method="amplitude", normalise=True) 131 | 132 | #data_train = eigen_encode_map(data_train, modes, photons) 133 | #data_test = eigen_encode_map(data_test, modes, photons) 134 | 135 | # pass through resevoir 136 | data_train = reservoir_map(data_train, modes, photons, pdesc, targets, temporal_num) 137 | data_test = reservoir_map(data_test, modes, photons, pdesc, targets, temporal_num) 138 | 139 | # save this mapped data so we don't have to recompute 140 | if save: 141 | np.savez(file_name, data_train=data_train, 142 | data_test=data_test, 143 | y_train=y_train, 144 | y_test=y_test) 145 | 146 | 147 | data_train = data_train[:1000] 148 | data_test = data_test[:1000] 149 | y_train = y_train[:1000] 150 | y_test = y_test[:1000] 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | # ------------------------------------------------------------ 159 | # Section 3: Define network topology 160 | # ------------------------------------------------------------ 161 | 162 | # define input layer of network, no longer a time sequence to be considered 163 | input_state = Input(batch_shape=[None, dim, dim], dtype=tf.complex64, name="state_input") 164 | 165 | 166 | # extract classical output state of resevoir 167 | #output = ULayer(modes, photons, force=True)(input_state) 168 | output = MeasureLayer(modes, photons, force=True)(input_state) 169 | # feed this into a small feedforward network 170 | 171 | output = Dense(units=20)(output) 172 | #output = ReLU(negative_slope=0.1, threshold=0.0)(output) 173 | output = Dense(units=20)(output) 174 | #output = ReLU(negative_slope=0.01, threshold=0.0)(output) 175 | output = Dense(units=3, activation="softmax", use_bias=True)(output) 176 | 177 | # define standard optimiser 178 | opt = optimizers.Adam(lr=init_lr) 179 | 180 | # define loss function 181 | loss = tf.keras.losses.CategoricalCrossentropy( 182 | from_logits=True, 183 | label_smoothing=0, 184 | reduction="auto", 185 | name="categorical_crossentropy") 186 | 187 | # define the model 188 | model = Model(inputs=input_state, output=output, name="Optical_Resevoir_Compute_Network") 189 | 190 | # ------------------------------------------------------------ 191 | # Section 4: Model compilation and training 192 | # ------------------------------------------------------------ 193 | 194 | # compile it using probability fidelity 195 | model.compile(optimizer=opt, 196 | loss=loss, 197 | metrics=["accuracy"]) 198 | 199 | # setup callbacks 200 | #name = datetime.now().strftime("%Y%m%d_%H%M%S") 201 | #logdir = "C:\\Users\\Joshua\\Projects\\Research\\Archive\\Logs\\fit\\" 202 | #tensorboard_callback = TensorBoard(log_dir=logdir+name, write_graph=True) 203 | schedule = PolynomialDecay(maxEpochs=epochs, initAlpha=init_lr, power=lr_pow) 204 | lr_callback = tf.keras.callbacks.LearningRateScheduler(schedule) 205 | 206 | model_checkpoint_callback = tf.keras.callbacks.ModelCheckpoint( 207 | save_weights_only=True, 208 | filepath=modelfile_name, 209 | save_freq="epoch", 210 | monitor='val_accuracy', 211 | mode='max', 212 | save_best_only=True) 213 | 214 | # map integer labels to 0,1,2 instead of 0,3,8 215 | y_train = integer_remap(y_train, [[3,1],[8,2]]) 216 | y_test = integer_remap(y_test, [[3,1],[8,2]]) 217 | 218 | # output model summary for visual checks 219 | model.summary() 220 | label_train = keras.utils.to_categorical(y_train, 3) 221 | label_test = keras.utils.to_categorical(y_test, 3) 222 | 223 | # train model if flag is set 224 | if train: 225 | model.fit(x=data_train, 226 | y=label_train, 227 | epochs=epochs, 228 | steps_per_epoch=len(data_train)//batch_size, 229 | verbose=1, 230 | validation_data=(data_test, label_test), 231 | validation_steps=1, 232 | callbacks=[lr_callback])#model_checkpoint_callback, 233 | 234 | cnn_results = model.evaluate(data_test, label_test) 235 | 236 | print(y_test[:10]) -------------------------------------------------------------------------------- /encode.py: -------------------------------------------------------------------------------- 1 | 2 | class QEncoder(object): 3 | """ 4 | Takes as input an NxMxW data array and applies a quantum encoding in given dimension 5 | """ 6 | 7 | # setup class ready for data encoding 8 | def __init__(self, modes, photons, topology=None, rand_encoding=False, density=False): 9 | # store all initialisation arguments 10 | # number of modes 11 | self.modes = modes 12 | # number of photons 13 | self.photons = photons 14 | # random encoding flag 15 | self.rand_encoding = rand_encoding 16 | # density operator output flag 17 | self.density = density 18 | # store topology (only needed for milano or other specific encoding) 19 | self.topology = topology 20 | # calculate system dimension 21 | self.dim = qop.fock_dim(self.modes, self.photons) 22 | 23 | 24 | def _granulate(self, data, levels=2, positive=True): 25 | """ 26 | Applies a granulation function to input data as preporcessing for the encoder. Assumes real data. 27 | """ 28 | # rescale data from [-infty, infty] range into zero to one using logistic function 29 | data_parse = 1/(1+np.exp(-data)) 30 | 31 | # rescale again if strictly positive range 32 | if positive: 33 | # logistic(0) = 0.5 34 | data_parse -= 0.5 35 | # renormalise 36 | data_parse /= np.max(data_parse) 37 | 38 | # now granulate according to desired number of levels 39 | inc = 1/levels 40 | lbnd = 0.0 41 | ubnd = 1/levels 42 | for i in range(levels): 43 | # assign value to interval 44 | truth_mask = (data_parselbnd) 45 | data_parse[truth_mask] = i 46 | # update 47 | lbnd += inc 48 | ubnd += inc 49 | 50 | # return as smallest non-boolean type available 51 | return data_parse.astype(np.uint8) 52 | 53 | def encode(self, data, method="amplitude", normalise=True, **kwargs): 54 | """ 55 | Performs requested encoding as well as basic data parsing 56 | """ 57 | 58 | # normalise if requested and be noisy about it 59 | if normalise: 60 | print("Data normalisation requested: Computing") 61 | data /= np.max(data) 62 | 63 | # separate map mathods for compartmentalisation 64 | if method is "eigen": 65 | print("Applying Gellman basis eigenstate map") 66 | return self._eigen_map(data=data, **kwargs) 67 | 68 | elif method is "milano": 69 | print("Applying milano topological map") 70 | return self._milano_map(data=data, **kwargs) 71 | 72 | elif method is "amplitude": 73 | print("Applying analog amplitude map") 74 | return self._amplitude_map(data=data, **kwargs) 75 | 76 | else: 77 | raise ValueError("Invalid encoding map {} requested".format(method)) 78 | 79 | def _eigen_map(self, data): 80 | """ 81 | Takes as input an NxMxW data array and applies a quantum encoding in given dimension 82 | """ 83 | 84 | # sanitise data to be accepted to our encoding scheme 85 | data = self._granulate(data, levels=2) 86 | 87 | # get shape of array and package as instance, input dimension and time series length 88 | N,in_dim,tlen = np.shape(data) 89 | 90 | # generate state encodings - assume modes and photons are enough 91 | encodings = basis_generator(self.dim) 92 | 93 | # perform in-place shuffle of encoding states if requested 94 | if rand_encoding: 95 | np.random.shuffle(encodings) 96 | 97 | # assert encoding is sufficent assuming binary data 98 | assert 2**in_dim<=len(encodings), "Woah hey woah, your system size is not sufficient to encode all states: {}<{}".format(len(encodings), 2**in_dim) 99 | 100 | # preallocate output encoding array as vector state or density operator 101 | if self.density: 102 | data_encode = np.zeros((N,tlen,self.dim,self.dim), dtype=np.complex64) 103 | else: 104 | data_encode = np.zeros((N,tlen,self.dim), dtype=np.complex64) 105 | 106 | # no doubt smarter way of computing this but it doesn't matter for the problem sizes we consider 107 | for i in range(N): 108 | if i % 100==0: 109 | print("{}/{} images encoded in quantum state space".format(i,N), end="\r") 110 | for j in range(tlen): 111 | # get classical state 112 | classical_state = data[i,:,j] 113 | 114 | # compute index of state to map it to 115 | ind = int(''.join(str(s) for s in classical_state), 2) 116 | 117 | # map classical binary data to a quantum state 118 | if density: 119 | state = encodings[ind,:].reshape([-1,1]) 120 | data_encode[i,j,:,:] = np.kron(state, dagger(state)) 121 | else: 122 | data_encode[i,j,:] = encodings[ind,:] 123 | 124 | return data_encode 125 | 126 | 127 | def _amplitude_map(self, data, levels=0,): 128 | """ 129 | Generates a standard amplitude encoding scheme. Hard to prepare in practice 130 | but acceptable for prototyping. 131 | """ 132 | 133 | # get shape of array and package as instance, input dimension and time series length 134 | N,in_dim,tlen = np.shape(data) 135 | 136 | # check whether to apply any kind of granulation effect 137 | if levels>0: 138 | print("Parse number set: applying logistic scaling and granulation") 139 | data = self._granulate(data, levels=levels) 140 | 141 | # preallocate output encoding array as vector state or density operator 142 | if self.density: 143 | data_encode = np.zeros((N,tlen,self.dim,self.dim), dtype=np.complex64) 144 | else: 145 | data_encode = np.zeros((N,tlen,self.dim), dtype=np.complex64) 146 | 147 | # generate computational basis elements 148 | basis = np.eye(self.dim, dtype=np.complex128) 149 | 150 | # convert each data element - likely a smart way to vectorise it but its not worth it here 151 | for i in range(N): 152 | if i % 100==0: 153 | print("{}/{} images encoded in quantum state space using amplitude map scheme".format(i,N), end="\r") 154 | 155 | # iterate over each column 156 | for t in range(tlen): 157 | # construct a new state vector 158 | state = np.zeros((self.dim,), dtype=np.complex128) 159 | state[:in_dim] = data[i,:,t] 160 | 161 | # normalise state vector 162 | state /= np.linalg.norm(state, 2) 163 | 164 | # assign to output set 165 | if self.density: 166 | state = state.reshape([-1,1]) 167 | data_encode[i,t,:,:] = np.kron(state, dagger(state)) 168 | else: 169 | data_encode[i,t,:] = state 170 | 171 | return data_encode 172 | 173 | 174 | def _milano_topology_gen(self): 175 | """ 176 | Creates a topology specifier for the Milano chip. 177 | """ 178 | 179 | # Topology is fixed, thus hardcoding 180 | first_layer = [[2,3],[6,7,],[10,11]] 181 | second_layer = [[1,2],[3,4],[5,6],[7,8],[9,10],[11,12]] 182 | third_layer = [[2,3],[4,5],[6,7],[8,9],[10,11]] 183 | fourth_layer = [[1,2],[3,4],[5,6],[7,8],[9,10],[11,12]] 184 | 185 | # compile topology list and return in reverse order 186 | return fourth_layer+third_layer+second_layer+first_layer 187 | 188 | 189 | def _milano_map(self, data, parse=False, masks=None, input_state=None, base=[0,2*np.pi/3,4*np.pi/3]): 190 | """ 191 | A highly specific encoding scheme using Milano optical chip - use eigenstate or amplitude encoding unless you know 192 | what you are doing! 193 | 194 | This encoding assumes a constant input state and then configures an optical map that 195 | """ 196 | # get shape of array and package as instance, input dimension and time series length 197 | N,in_dim,tlen = np.shape(data) 198 | 199 | # granulate data to be accepted to our encoding scheme 200 | 201 | if parse: 202 | print("Parse flag set: applying logistic scaling and granulation") 203 | data = self._granulate(data, levels=len(base)) 204 | 205 | # generate initial input state if not defined (assumes mode and photon numbers) 206 | if input_state is None: 207 | # 12 modes, 3 photons 208 | nstate = [0,0,1,0,0,0,1,0,0,0,1,0] 209 | # make use of qfunk optical library 210 | input_state = qop.optical_state_gen(m_num=self.modes, p_num=self.photons, nstate=nstate, density=False) 211 | 212 | # generate topology if not set 213 | if self.topology is None: 214 | self.topology = self._milano_topology_gen() 215 | 216 | # generate a set of masks if none are given 217 | if masks is None: 218 | masks = mask_generator(in_dim, 3) 219 | 220 | # initialise a dictionary so generated gates don't have to be rebuilt 221 | gate_collector = {} 222 | 223 | # preallocate output encoding array as vector state or density operator 224 | if self.density: 225 | data_encode = np.empty((N,tlen,self.dim,self.dim), dtype=np.complex128) 226 | else: 227 | data_encode = np.empty((N,tlen,self.dim), dtype=np.complex128) 228 | 229 | 230 | # iterate over each and every input image 231 | for i in range(N): 232 | if i % 100==0: 233 | print("{}/{} images encoded in quantum state space using Milano scheme".format(i,N), end="\r") 234 | # iterate over each column 235 | for t in range(tlen): 236 | # get the classical data to be encoded 237 | classical_state = list(data[i,:,t]) 238 | 239 | # convert to string description 240 | state_string = ''.join(str(s) for s in classical_state) 241 | 242 | # check if we have already generated the resultant gate 243 | if state_string in gate_collector: 244 | U = gate_collector[state_string] 245 | else: 246 | # compute trinary mapping 247 | phase_shifters = [base[el] for el in classical_state] 248 | 249 | # apply weighted mask for each pre-encoder MZI 250 | for mask in masks: 251 | # compute maximum value possible for mask 252 | max_val = np.sum(mask*(len(base)-1)) 253 | # compute unscaled phase value for pre-encoder 254 | phase_val = (base[-1]-base[0])*mask @ classical_state 255 | # add to end of phase shifters and rescale with range 256 | phase_shifters.append(phase_val/max_val) 257 | 258 | 259 | # now iterate over topology to generate encoder 260 | 261 | # compute unitary from values 262 | #T(self.targets[0], self.targets[1], theta, theta, self.modes) 263 | U = np.eye(self.dim) 264 | 265 | # save computed unitary for repeated use 266 | gate_collector[state_string] = U 267 | 268 | # apply computed unitary to input state 269 | state = U @ input_state 270 | 271 | if self.density: 272 | data_encode[i,t,:,:] = np.kron(state, dagger(state)) 273 | else: 274 | data_encode[i,t,:] = state 275 | 276 | return data_encode 277 | 278 | 279 | -------------------------------------------------------------------------------- /utility.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from keras.layers import Input, Layer 4 | import collections 5 | from math import log, exp 6 | 7 | import qfunk.qoptic as qop 8 | from qinfo.qinfo import dagger, haar_sample, multikron, gellman_gen, random_U 9 | from ucell import * 10 | from ucell.utility import * 11 | from test_data import * 12 | 13 | 14 | 15 | 16 | class QMemristor(object): 17 | """ 18 | Advanced data map that implements a stack of quantum memristors acting in parallel. 19 | each memristor takes as input 4 modes - two being the input state, a third 20 | being the detector input (instantiated to no input) and a final sacrifical mode 21 | that contains the destroyed photons. 22 | 23 | pdesc = {'theta_init':None,'MZI_target': [m,m+1],'proj_mode': n!=m,m+1, 'sacrificial_mode': o!=n,m,m+1} 24 | """ 25 | 26 | def __init__(self, modes, photons, pdesc, force=False, **kwargs): 27 | # layer identifier 28 | self.id = "qmemristor" 29 | # total number of modes 30 | self.modes = modes 31 | # total number of photons 32 | self.photons = photons 33 | # number of variables on SU(modes) 34 | self.vars = (self.modes**2 - self.modes)//2 35 | # projection operator description 36 | self.pdesc = pdesc 37 | # length of time series 38 | self.tlen = self.pdesc["tlen"] 39 | # emergent dimension 40 | self.input_dim = self.output_dim = comb( 41 | self.modes+self.photons-1, self.photons, exact=True) 42 | # catch extreme cases 43 | if self.input_dim > 100 and not force: 44 | raise ValueError( 45 | "System dimension is large ({}), decrease system size or set force flag to True".format(self.input_dim)) 46 | 47 | # define memory state vector 48 | self.mem_states = np.zeros((self.tlen), dtype=np.float64) 49 | # initialise unitary array 50 | self.unitary_array = np.zeros((self.tlen, self.input_dim, self.input_dim), dtype=np.complex128) 51 | 52 | # initial value of phase shifter 53 | 54 | if 'theta_init' not in self.pdesc or self.pdesc['theta_init']==None: 55 | self.mem_states[0] = np.random.rand()*np.pi 56 | else: 57 | self.mem_states[0] = self.pdesc['theta_init'] 58 | 59 | # save symmetric map for dilation to complete Fock space (much, much faster than permanents) 60 | self.S = symmetric_map(self.modes, self.photons) 61 | # build initial unitary array 62 | self.update(time_index=0) 63 | # build projector tensor for measure/prepare stage 64 | self.projector_build() 65 | 66 | # memory state vector 67 | super(QMemristor, self).__init__(**kwargs) 68 | 69 | def projector_build(self): 70 | """ 71 | Construct quantum memristor with stateful phase shifter and projective measure 72 | """ 73 | 74 | # generate the projector that takes the memristor from a given quantum state to its average state 75 | # generate null vector that ignores photons not in measurement mode 76 | null = np.zeros((self.modes,)) 77 | null[self.pdesc['proj_mode']] = 1 78 | 79 | # generate basis set 80 | basis = np.eye(self.input_dim) 81 | # and associate with number states 82 | nstates = number_states(self.modes, self.photons) 83 | # construct projector list 84 | projectors = [] 85 | # construct completeness list 86 | complete = [] 87 | # construct photon mode number list 88 | self.proj_photon = [] 89 | 90 | # iterate over all basis states 91 | for i in range(self.input_dim): 92 | # get number state 93 | fvec = nstates[i,:] 94 | # remove elements we don't care about 95 | fvec = null*fvec 96 | 97 | # skip nullified basis elements already completed (why we work with integers) 98 | if list(fvec) in complete: 99 | continue 100 | else: 101 | # add to completed projector list 102 | complete.append(list(fvec)) 103 | 104 | # generate isometric projector 105 | M = np.zeros((self.input_dim, self.input_dim)) 106 | 107 | # iterate over the number states 108 | for j in range(self.input_dim): 109 | # if feature vector matches a basis in the modes we care about 110 | if (fvec == (null*nstates[j,:])).all(): 111 | 112 | #TODO: allow for inability to perform photon counting statistics 113 | # feature vector must be mapped to one photon in measurement mode and additional photons in sacrificial mode 114 | # collapse superpositions if no more than one photon 115 | if sum(fvec)>=0: 116 | M += np.kron(basis[:,j].reshape(self.input_dim,1), basis[:,j].reshape(1,self.input_dim)) 117 | # more than one photon in measurement mode, map to 118 | 119 | # add that projector to the list 120 | projectors.append(M) 121 | # add photon number associated with projection 122 | self.proj_photon.append(fvec[self.pdesc['proj_mode']]) 123 | 124 | # repackage into projector tensor 125 | self.proj = np.zeros((len(projectors), self.input_dim, self.input_dim)) 126 | # iterate over all projectors 127 | for i in range(len(projectors)): 128 | self.proj[i,:,:] = projectors[i] 129 | 130 | # set build flag 131 | self.built = True 132 | 133 | def call(self, input_states): 134 | """ 135 | Method that applies memristor element to encoded quantum state. Takes as input a single quantum 136 | state sequence in density operator form 137 | """ 138 | 139 | # some basic data checking to catch annoying to track errors 140 | if len(input_states) != self.tlen: 141 | print("Dimension mismatch: Specified time series length does not match data length {}!={}".format(self.tlen, len(input_states))) 142 | 143 | # iteratively apply memristor for each time interval and produce next output state in sequence 144 | for i in range(self.tlen): 145 | # get input for time step 146 | time_input = np.squeeze(input_states[i,:,:]) 147 | 148 | # get unitary for this time step 149 | unitary = self.unitary_array[i,:,:] 150 | 151 | # apply unitary to density operator 152 | output_state = unitary @ time_input @ dagger(unitary) 153 | 154 | # apply unitary transform as left hand operator 155 | #left = np.einsum('ij,jl->il', unitary, time_input)u 156 | # right hand operator 157 | #out = np.einsum('il,lj->ij', left, dagger(unitary)) 158 | 159 | # apply projection operators to input state 160 | #compute effect of measurement projector on modes specified by photonic projectors 161 | left = np.einsum('ijk,kl->ijl', self.proj, output_state) 162 | # can skip adjoint calculation since projectors are all real diagonals (does that seem right?) 163 | right = np.einsum('ijl,ilk->ijk', left, self.proj) #TODO 164 | # collapse projector outcome sum into single array, taking of batch broadcasting rules 165 | out = np.sum(right, axis=0) 166 | 167 | # add state to output tensor 168 | input_states[i,:,:] = out 169 | 170 | if i+1 100 and not force: 224 | raise ValueError( 225 | "System dimension is large ({}), decrease system size or set force flag to True".format(self.input_dim)) 226 | 227 | # build operator for class 228 | self.build() 229 | 230 | super(BSArray, self).__init__(**kwargs) 231 | 232 | def build(self): 233 | """ 234 | Construct fully connected beam splitter array 235 | """ 236 | 237 | # create beam splitter decomposition 238 | bms_spec = clements_phase_end(np.eye(self.modes))[0] 239 | 240 | # retest to beamsplitters with no phase effect (not that it matter) 241 | for bm in bms_spec: 242 | bm[2] = 0 243 | bm[3] = np.random.rand() 244 | 245 | # construct single photon unitary 246 | self.unitary = clements_stitch(bms_spec, [1.0]*self.modes) 247 | # map unitary to multiphoton space if required 248 | if self.photons > 1: 249 | # save symmetric map for dilation to complete Fock space (much, much faster than permanents) 250 | S = symmetric_map(self.modes, self.photons) 251 | 252 | self.unitary = S @ multikron(self.unitary, self.photons) @ np.transpose(S) 253 | 254 | # set build flag 255 | self.built = True 256 | 257 | 258 | def call(self, input_states): 259 | """This is where the layer's logic lives. 260 | Arguments: 261 | inputs: Input tensor, or list/tuple of input tensors. 262 | **kwargs: Additional keyword arguments. 263 | 264 | Returns: 265 | A tensor or list/tuple of tensors. 266 | """ 267 | 268 | # apply unitary channel as left operator 269 | input_left = np.einsum('ij,bkjl->bkil', self.unitary, input_states) 270 | # now as right hand operator and return 271 | input_states = np.einsum('bkil,lj->bkij', input_left, dagger(self.unitary)) 272 | 273 | return input_states 274 | 275 | 276 | 277 | class MeasureLayer(Layer): 278 | """ 279 | Advanced layer that implements a stack of quantum memristors acting in parallel. 280 | each memristor takes as input 4 modes - two being the input state, a third 281 | being the detector input (instantiated to no input) and a final sacrifical mode 282 | that contains the destroyed photons. 283 | 284 | pdesc = {'theta_init':None,'MZI_target': [m,m+1],'proj_mode': n!=m,m+1, 'sacrificial_mode': o!=n,m,m+1} 285 | """ 286 | 287 | def __init__(self, modes, photons, force=False, **kwargs): 288 | # layer identifier 289 | self.id = "measure" 290 | # total number of modes 291 | self.modes = modes 292 | # total number of photons 293 | self.photons = photons 294 | # emergent dimension 295 | self.input_dim = self.output_dim = comb( 296 | self.modes+self.photons-1, self.photons, exact=True) 297 | 298 | #self.output_dim /= 2 299 | 300 | # catch extreme cases 301 | if self.input_dim > 100 and not force: 302 | raise ValueError( 303 | "System dimension is large ({}), decrease system size or set force flag to True".format(self.input_dim)) 304 | 305 | super(MeasureLayer, self).__init__(**kwargs) 306 | 307 | 308 | def build(self, input_shape): 309 | """ 310 | Build method for layer 311 | """ 312 | 313 | # set build flag 314 | self.built = True 315 | 316 | 317 | def call(self, inputs): 318 | """This is where the layer's logic lives. 319 | Arguments: 320 | inputs: Input tensor, or list/tuple of input tensors. 321 | **kwargs: Additional keyword arguments. 322 | 323 | Returns: 324 | A tensor or list/tuple of tensors. 325 | """ 326 | # get diagonal elements - corresponds to photon counting 327 | diag = tf.math.real(tf.linalg.diag_part(inputs))[:,::] 328 | 329 | #scalar = 1/tf.reduce_sum(diag, axis=1) 330 | 331 | # rescale and return 332 | return diag 333 | 334 | 335 | def compute_output_shape(self, input_shape): 336 | return tf.TensorShape([None, self.output_dim]) 337 | 338 | def get_config(self): 339 | base_config = super(MeasureLayer, self).get_config() 340 | base_config['output_dim'] = self.output_dim 341 | base_config['modes'] = self.modes 342 | base_config['photons'] = self.photons 343 | return base_config 344 | 345 | @classmethod 346 | def from_config(cls, config): 347 | return cls(**config) 348 | 349 | @property 350 | def output_size(self): 351 | return [self.output_dim] 352 | 353 | 354 | class LearningRateDecay(): 355 | """ 356 | """ 357 | def plot(self, epochs, title="Learning Rate Schedule"): 358 | # compute the set of learning rates for each corresponding 359 | # epoch 360 | lrs = [self(i) for i in epochs] 361 | 362 | # the learning rate schedule 363 | plt.style.use("ggplot") 364 | plt.figure() 365 | plt.plot(epochs, lrs) 366 | plt.title(title) 367 | plt.xlabel("Epoch #") 368 | plt.ylabel("Learning Rate") 369 | 370 | class PolynomialDecay(LearningRateDecay): 371 | def __init__(self, maxEpochs=100, initAlpha=0.01, power=1.0): 372 | # store the maximum number of epochs, base learning rate, 373 | # and power of the polynomial 374 | self.maxEpochs = maxEpochs 375 | self.initAlpha = initAlpha 376 | self.power = power 377 | 378 | def __call__(self, epoch): 379 | # compute the new learning rate based on polynomial decay 380 | decay = (1 - (epoch / float(self.maxEpochs))) ** self.power 381 | alpha = self.initAlpha * decay 382 | 383 | # return the new learning rate 384 | return float(alpha) 385 | 386 | def basis_generator(dim): 387 | """ 388 | Generates a set of orthogonal basis elements in qubit basis 389 | """ 390 | 391 | # generate Pauli basis 392 | basis = gellman_gen(dim) 393 | 394 | # normalised states 395 | state_encoding = np.zeros((dim**3-dim,dim),dtype=np.complex64) 396 | 397 | # extract eigenvectors 398 | cnt = 0 399 | for i in range(dim**2-1): 400 | w,v = np.linalg.eigh(basis[i,:,:]) 401 | for j in range(dim): 402 | state_encoding[cnt,:] = v[:,j] 403 | cnt +=1 404 | 405 | return state_encoding 406 | 407 | 408 | class QEncoder(object): 409 | """ 410 | Takes as input an NxMxW data array and applies a quantum encoding in given dimension 411 | """ 412 | 413 | # setup class ready for data encoding 414 | def __init__(self, modes, photons, topology=None, density=False): 415 | # store all initialisation arguments 416 | # number of modes 417 | self.modes = modes 418 | # number of photons 419 | self.photons = photons 420 | # density operator output flag 421 | self.density = density 422 | # store topology (only needed for milano or other specific encoding) 423 | self.topology = topology 424 | # calculate system dimension 425 | self.dim = qop.fock_dim(self.modes, self.photons) 426 | # define random vector map 427 | self.vector_map = None 428 | 429 | def _granulate(self, data, levels=2, positive=True): 430 | """ 431 | Applies a granulation function to input data as preporcessing for the encoder. Assumes real data. 432 | """ 433 | # rescale data from [-infty, infty] range into zero to one using logistic function 434 | data_parse = 1/(1+np.exp(-data)) 435 | 436 | # rescale again if strictly positive range 437 | if positive: 438 | # logistic(0) = 0.5 439 | data_parse -= 0.5 440 | # renormalise 441 | data_parse /= np.max(data_parse) 442 | 443 | # now granulate according to desired number of levels 444 | inc = 1/levels 445 | lbnd = 0.0 446 | ubnd = 1/levels 447 | for i in range(levels): 448 | # assign value to interval 449 | truth_mask = (data_parselbnd) 450 | data_parse[truth_mask] = i 451 | # update 452 | lbnd += inc 453 | ubnd += inc 454 | 455 | # return as smallest non-boolean type available 456 | return data_parse.astype(np.uint8) 457 | 458 | def encode(self, data, method="amplitude", normalise=True): 459 | """ 460 | Performs requested encoding as well as basic data parsing 461 | """ 462 | 463 | # normalise if requested and be noisy about it 464 | if normalise: 465 | print("Data normalisation has been requested") 466 | data /= np.max(data) 467 | 468 | # separate map mathods for compartmentalisation 469 | if method is "eigen": 470 | print("Applying Gellman basis eigenstate map") 471 | return self._eigen_map(data=data) 472 | 473 | elif method is "milano": 474 | print("Applying milano topological map") 475 | return self._milano_map(data=data) 476 | 477 | elif method is "amplitude": 478 | 479 | if self.vector_map is None: 480 | print("Random vector map not initialised, creating one now") 481 | # get shape of input data 482 | _,in_dim,_ = np.shape(data) 483 | # generate a random map 484 | self.vector_map = np.random.choice(range(self.dim), size=in_dim) 485 | 486 | print("Applying analog amplitude map") 487 | return self._amplitude_map(data=data) 488 | 489 | else: 490 | raise ValueError("Invalid encoding map {} requested".format(method)) 491 | 492 | def _eigen_map(self, data, rand_encoding=False): 493 | """ 494 | Takes as input an NxMxW data array and applies a quantum encoding in given dimension 495 | """ 496 | 497 | # sanitise data to be accepted to our encoding scheme 498 | data = self._granulate(data, levels=2) 499 | 500 | # get shape of array and package as instance, input dimension and time series length 501 | N,in_dim,tlen = np.shape(data) 502 | 503 | # generate state encodings - assume modes and photons are enough 504 | encodings = basis_generator(self.dim) 505 | 506 | # perform in-place shuffle of encoding states if requested 507 | if rand_encoding: 508 | np.random.shuffle(encodings) 509 | 510 | # assert encoding is sufficent assuming binary data 511 | assert 2**in_dim<=len(encodings), "Woah hey woah, your system size is not sufficient to encode all states: {}<{}".format(len(encodings), 2**in_dim) 512 | 513 | # preallocate output encoding array as vector state or density operator 514 | if self.density: 515 | data_encode = np.zeros((N,tlen,self.dim,self.dim), dtype=np.complex64) 516 | else: 517 | data_encode = np.zeros((N,tlen,self.dim), dtype=np.complex64) 518 | 519 | # no doubt there is a smarter way of computing this but it doesn't matter for such small problem sizes 520 | for i in range(N): 521 | if i % 100==0: 522 | print("{}/{} images encoded in quantum state space".format(i,N), end="\r") 523 | for j in range(tlen): 524 | # get classical state 525 | classical_state = data[i,:,j] 526 | 527 | # compute index of state to map it to 528 | ind = int(''.join(str(s) for s in classical_state), 2) 529 | 530 | # map classical binary data to a quantum state 531 | if self.density: 532 | state = encodings[ind,:].reshape([-1,1]) 533 | data_encode[i,j,:,:] = np.kron(state, dagger(state)) 534 | else: 535 | data_encode[i,j,:] = encodings[ind,:] 536 | 537 | return data_encode 538 | 539 | 540 | def _amplitude_map(self, data, levels=0,): 541 | """ 542 | Generates a standard amplitude encoding scheme. Hard to prepare in practice 543 | but acceptable for prototyping. 544 | """ 545 | 546 | # get shape of array and package as instance, input dimension and time series length 547 | N,in_dim,tlen = np.shape(data) 548 | 549 | # check whether to apply any kind of granulation effect 550 | if levels>0: 551 | print("Parse number set: applying logistic scaling and granulation") 552 | data = self._granulate(data, levels=levels) 553 | 554 | # preallocate output encoding array as vector state or density operator 555 | if self.density: 556 | data_encode = np.zeros((N,tlen,self.dim,self.dim), dtype=np.complex64) 557 | else: 558 | data_encode = np.zeros((N,tlen,self.dim), dtype=np.complex64) 559 | 560 | # generate computational basis elements 561 | basis = np.eye(self.dim, dtype=np.complex128) 562 | 563 | # convert each data element - likely a smart way to vectorise it but its not worth it here 564 | for i in range(N): 565 | if i % 100==0: 566 | print("{}/{} images encoded in quantum state space using amplitude map scheme".format(i,N), end="\r") 567 | 568 | # iterate over each column 569 | for t in range(tlen): 570 | # construct a new state vector 571 | state = np.zeros((self.dim,), dtype=np.complex128) 572 | # phase encoding to random states 573 | state[self.vector_map] = np.exp(1j*data[i,:,t]*2*np.pi) 574 | # normalise state vector 575 | state /= np.linalg.norm(state, 2) 576 | 577 | # assign to output set 578 | if self.density: 579 | state = state.reshape([-1,1]) 580 | data_encode[i,t,:,:] = np.kron(state, dagger(state)) 581 | else: 582 | data_encode[i,t,:] = state 583 | 584 | print() 585 | return data_encode 586 | 587 | 588 | def _milano_topology_gen(self): 589 | """ 590 | Creates a topology specifier for the Milano chip. 591 | """ 592 | 593 | # Topology is fixed, thus hardcoding 594 | first_layer = [[2,3],[6,7,],[10,11]] 595 | second_layer = [[1,2],[3,4],[5,6],[7,8],[9,10],[11,12]] 596 | third_layer = [[2,3],[4,5],[6,7],[8,9],[10,11]] 597 | fourth_layer = [[1,2],[3,4],[5,6],[7,8],[9,10],[11,12]] 598 | 599 | # compile topology list and return in reverse order 600 | return fourth_layer+third_layer+second_layer+first_layer 601 | 602 | 603 | def _milano_map(self, data, parse=False, masks=None, input_state=None, base=[0,2*np.pi/3,4*np.pi/3]): 604 | """ 605 | A highly specific encoding scheme using Milano optical chip - use eigenstate or amplitude encoding unless you know 606 | what you are doing! 607 | 608 | This encoding assumes a constant input state and then configures an optical map that 609 | """ 610 | # get shape of array and package as instance, input dimension and time series length 611 | N,in_dim,tlen = np.shape(data) 612 | 613 | # granulate data to be accepted to our encoding scheme 614 | 615 | if parse: 616 | print("Parse flag set: applying logistic scaling and granulation") 617 | data = self._granulate(data, levels=len(base)) 618 | 619 | # generate initial input state if not defined (assumes mode and photon numbers) 620 | if input_state is None: 621 | # 12 modes, 3 photons 622 | nstate = [0,0,1,0,0,0,1,0,0,0,1,0] 623 | # make use of qfunk optical library 624 | input_state = qop.optical_state_gen(m_num=self.modes, p_num=self.photons, nstate=nstate, density=False) 625 | 626 | # generate topology if not set 627 | if self.topology is None: 628 | self.topology = self._milano_topology_gen() 629 | 630 | # generate a set of masks if none are given 631 | if masks is None: 632 | masks = mask_generator(in_dim, 3) 633 | 634 | # initialise a dictionary so generated gates don't have to be rebuilt 635 | gate_collector = {} 636 | 637 | # preallocate output encoding array as vector state or density operator 638 | if self.density: 639 | data_encode = np.empty((N,tlen,self.dim,self.dim), dtype=np.complex128) 640 | else: 641 | data_encode = np.empty((N,tlen,self.dim), dtype=np.complex128) 642 | 643 | 644 | # iterate over each and every input image 645 | for i in range(N): 646 | if i % 100==0: 647 | print("{}/{} images encoded in quantum state space using Milano scheme".format(i,N), end="\r") 648 | # iterate over each column 649 | for t in range(tlen): 650 | # get the classical data to be encoded 651 | classical_state = list(data[i,:,t]) 652 | 653 | # convert to string description 654 | state_string = ''.join(str(s) for s in classical_state) 655 | 656 | # check if we have already generated the resultant gate 657 | if state_string in gate_collector: 658 | U = gate_collector[state_string] 659 | else: 660 | # compute trinary mapping 661 | phase_shifters = [base[el] for el in classical_state] 662 | 663 | # apply weighted mask for each pre-encoder MZI 664 | for mask in masks: 665 | # compute maximum value possible for mask 666 | max_val = np.sum(mask*(len(base)-1)) 667 | # compute unscaled phase value for pre-encoder 668 | phase_val = (base[-1]-base[0])*mask @ classical_state 669 | # add to end of phase shifters and rescale with range 670 | phase_shifters.append(phase_val/max_val) 671 | 672 | 673 | # now iterate over topology to generate encoder 674 | 675 | # compute unitary from values 676 | #T(self.targets[0], self.targets[1], theta, theta, self.modes) 677 | U = np.eye(self.dim) 678 | 679 | # save computed unitary for repeated use 680 | gate_collector[state_string] = U 681 | 682 | # apply computed unitary to input state 683 | state = U @ input_state 684 | 685 | if self.density: 686 | data_encode[i,t,:,:] = np.kron(state, dagger(state)) 687 | else: 688 | data_encode[i,t,:] = state 689 | 690 | return data_encode 691 | 692 | 693 | 694 | 695 | def eigen_encode_map(data, modes, photons, rand_encoding=False, density=True): 696 | """ 697 | Takes as input an NxMxW data array and applies a quantum encoding in given dimension 698 | """ 699 | 700 | # get shape of array and package as instance, input dimension and time series length 701 | N,in_dim,tlen = np.shape(data) 702 | 703 | # calculate system dimension 704 | dim = comb(modes+photons-1, photons, exact=True) 705 | 706 | # generate state encodings - assume modes and photons are enough 707 | encodings = basis_generator(dim) 708 | 709 | # perform in-place shuffle of encoding states if requested 710 | if rand_encoding: 711 | np.random.shuffle(encodings) 712 | 713 | # assert encoding is sufficent assuming binary data 714 | assert 2**in_dim<=len(encodings), "Woah hey woah, your system size is not sufficent to encode all states: {}<{}".format(len(encodings), 2**in_dim) 715 | 716 | # preallocate output encoding array as vector state or density operator 717 | if density: 718 | data_encode = np.zeros((N,tlen,dim,dim), dtype=np.complex64) 719 | else: 720 | data_encode = np.zeros((N,tlen,dim), dtype=np.complex64) 721 | 722 | # no doubt smarter way of computing this but it doesn't matter for the problem sizes we consider 723 | for i in range(N): 724 | if i % 100==0: 725 | print("{}/{} images encoded in quantum state space".format(i,N), end="\r") 726 | for j in range(tlen): 727 | # get classical state 728 | classical_state = data[i,:,j] 729 | 730 | # compute index of state to map it to 731 | ind = int(''.join(str(s) for s in classical_state), 2) 732 | 733 | # map classical binary data to a quantum state 734 | if density: 735 | state = encodings[ind,:].reshape([-1,1]) 736 | data_encode[i,j,:,:] = np.kron(state, dagger(state)) 737 | else: 738 | data_encode[i,j,:] = encodings[ind,:] 739 | print() 740 | return data_encode 741 | 742 | 743 | 744 | def reservoir_map(data, modes, photons, pdesc, targets, temporal_num): 745 | """ 746 | Applies resevoir channel to input data sequence 747 | """ 748 | # generate a Hadamard channel first 749 | hadamard_layer = BSArray(modes, photons, force=True) 750 | 751 | # iterate through each layer of resevoir 752 | for layer_num in range(temporal_num): 753 | 754 | # apply hadamard channel 755 | print("Applying Hadamard layer") 756 | data = hadamard_layer.call(data) 757 | 758 | # iterate over list of targets for resevoir layer 759 | for tnum,target in enumerate(targets): 760 | continue 761 | # generate memristor element class 762 | pdesc["MZI_target"] = target[:2] 763 | pdesc["proj_mode"] = target[-1] 764 | qmemristor = QMemristor(modes=modes, photons=photons, pdesc=pdesc, force=True) 765 | 766 | # apply layer element to every data sample 767 | for state_num in range(len(data)): 768 | if state_num % 10 ==0: 769 | print("Computing {}/{}:{}/{}:{}/{} component of reservoir channel".format(state_num, len(data), tnum, len(targets), layer_num, temporal_num), end="\r") 770 | # propagate all input data through network, one sample at a time 771 | data[state_num,:,:,:] = qmemristor.call(data[state_num,:,:,:]) 772 | 773 | print() 774 | # only need last output state as a time series 775 | return np.squeeze(data[:,-1,:,:]) 776 | 777 | 778 | 779 | def filter_36(x, y): 780 | keep = (y == 0) | (y==2) | (y==3)# | (y == 4) | (y == 5) | (y==6) 781 | x, y = x[keep], y[keep] 782 | return x,y 783 | 784 | 785 | def integer_remap(data, maps): 786 | """ 787 | Sets all values in data to values in maps - not for floats 788 | """ 789 | 790 | for item in maps: 791 | # set all values of a particular kind to another 792 | data[data==item[0]] = item[1] 793 | return data 794 | 795 | 796 | 797 | def remove_contradicting(xs, ys): 798 | mapping = collections.defaultdict(set) 799 | # Determine the set of labels for each unique image: 800 | for x,y in zip(xs,ys): 801 | mapping[tuple(x.flatten())].add(y) 802 | 803 | new_x = [] 804 | new_y = [] 805 | for x,y in zip(xs, ys): 806 | labels = mapping[tuple(x.flatten())] 807 | if len(labels) == 1: 808 | new_x.append(x) 809 | new_y.append(list(labels)[0]) 810 | else: 811 | # Throw out images that match more than one label. 812 | pass 813 | 814 | num_3 = sum(1 for value in mapping.values() if True in value) 815 | num_6 = sum(1 for value in mapping.values() if False in value) 816 | num_both = sum(1 for value in mapping.values() if len(value) == 2) 817 | 818 | print("Number of unique images:", len(mapping.values())) 819 | print("Number of 3s: ", num_3) 820 | print("Number of 6s: ", num_6) 821 | print("Number of contradictory images: ", num_both) 822 | print() 823 | print("Initial number of examples: ", len(xs)) 824 | print("Remaining non-contradictory examples: ", len(new_x)) 825 | 826 | return np.array(new_x), np.array(new_y) 827 | 828 | 829 | 830 | def binarise(data): 831 | """ 832 | Converts input image data to a classical binary representation. assumes uint8 input 833 | """ 834 | 835 | data *= 1/np.max(data) 836 | # set floor and ceiling values 837 | data[data>0.5] = 1 838 | data[data<=0.5] = 0 839 | 840 | return data.astype(np.uint8) 841 | 842 | 843 | 844 | def probfid2(rho, gamma): 845 | """ 846 | Computes output probability distribution of two density operators and computes 847 | their relative entropy 848 | """ 849 | return tf.keras.losses.KLD(rho, gamma) 850 | 851 | def ent_gen(dim, vec=False): 852 | """ 853 | Generates a maximally entangled bi-partite system each of dimension dim 854 | """ 855 | 856 | # pre allocate entangled state array 857 | ent = np.zeros((dim**2,1),dtype=np.complex128) 858 | 859 | # iterate over each basis element 860 | for i in range(dim): 861 | # generate computaional basis element 862 | comput_el = np.zeros((dim, 1), dtype=np.complex128) 863 | # assign value 864 | comput_el[i] = 1.0 865 | 866 | # add to state 867 | ent += np.kron(comput_el, comput_el) 868 | 869 | if vec: 870 | return ent 871 | else: 872 | return np.kron(ent, dagger(ent))/dim 873 | 874 | def entanglement_gen(dim, num=100, partition=0.5, embed_dim=None): 875 | """ 876 | Generates a set of training and testing data for bipartite entanglement witnesses. 877 | dim gives the dimension of the subsystem and embed dim places the generated states 878 | into larger Hilbert space 879 | """ 880 | 881 | 882 | # generate entangled state 883 | ent_state = ent_gen(dim) 884 | 885 | # generate base seperable state 886 | sep_state = np.ones((dim**2, dim**2),dtype=np.complex128)/dim**2 887 | 888 | # check if embedding must occur 889 | if embed_dim is not None: 890 | assert embed_dim>=dim**2, "embedded dimension is of insufficient size" 891 | data_dim = embed_dim 892 | else: 893 | data_dim = dim**2 894 | 895 | # initialise data constructs 896 | ent_states = np.zeros((num,1, data_dim, data_dim), dtype=np.complex128) 897 | sep_states = np.zeros((num,1, data_dim, data_dim), dtype=np.complex128) 898 | 899 | # iterate over it all 900 | for i in range(num): 901 | # generate random local unitaries 902 | #U_ent = np.kron(np.squeeze(random_U(dim=dim, num=1)), np.squeeze(random_U(dim=dim, num=1))) 903 | U_sep = np.kron(np.squeeze(random_U(dim=dim, num=1)), np.squeeze(random_U(dim=dim, num=1))) 904 | # apply to entangled state and add to collection 905 | ent_states[i,0,:dim**2,:dim**2] = np.squeeze(haar_sample(dim=dim**2,num=1)) #U_ent @ ent_state @ dagger(U_ent) 906 | # apply seperable state and add to collection 907 | sep_states[i,0,:dim**2,:dim**2] = U_sep @ sep_state @ dagger(U_sep) 908 | 909 | # generate labels 910 | sep_labels = np.zeros((num, 1), dtype=int) 911 | ent_labels = np.ones((num, 1), dtype=int) 912 | 913 | # concatenate everything 914 | entangled_data = np.concatenate([sep_states, ent_states], axis=0) 915 | entangled_labels = np.concatenate([sep_labels, ent_labels], axis=0) 916 | 917 | # shuffle everything using repeateable rng state 918 | seed_state = np.random.get_state() 919 | np.random.shuffle(entangled_data) 920 | np.random.set_state(seed_state) 921 | np.random.shuffle(entangled_labels) 922 | 923 | # apply partition 924 | partition_index = int(partition*len(entangled_data)) 925 | # training data 926 | train_data = entangled_data[:partition_index] 927 | train_label = entangled_labels[:partition_index] 928 | 929 | # testing data 930 | test_data = entangled_data[partition_index:] 931 | test_label = entangled_labels[partition_index:] 932 | 933 | # return it all 934 | return train_data,train_label,test_data,test_label 935 | 936 | 937 | 938 | 939 | if __name__ == '__main__': 940 | pdesc = {'modes':3, 'photons':2} 941 | modes = 3 942 | photons = 2 943 | print(number_states(modes, photons)) 944 | new_data = encode_map(data, modes, photons) 945 | -------------------------------------------------------------------------------- /ucell/utility.py: -------------------------------------------------------------------------------- 1 | 2 | import tensorflow as tf 3 | import math 4 | import numpy as np 5 | import string 6 | import scipy 7 | from math import isnan 8 | from scipy.special import comb 9 | from qinfo.qinfo import dagger,dirsum, haar_sample, rhoplot, tf_multikron 10 | from contextlib import contextmanager 11 | from ucell.operators import op 12 | import sys 13 | import os 14 | 15 | 16 | 17 | @contextmanager 18 | def silence_stdout(): 19 | new_target = open(os.devnull, "w") 20 | old_target = sys.stdout 21 | sys.stdout = new_target 22 | try: 23 | yield new_target 24 | finally: 25 | sys.stdout = old_target 26 | 27 | def tf_clements_stitch(beam_spec, theta, phi, diag, rev=tf.constant(True)): 28 | """ 29 | Computes the unitary given by a clements decomposition with tensorflow compliance 30 | """ 31 | 32 | nmax = beam_spec[0][-1] 33 | 34 | # construct adjoint of the phase shifts 35 | U = tf.linalg.diag(tf.exp(tf.complex(0.0, diag))) 36 | 37 | # iterate over specified beam splitter arrays 38 | for i, spec in enumerate(beam_spec): 39 | # construct nd scatter indices 40 | m, n = spec[:2] 41 | 42 | # construct indices to update with beam splitter params 43 | indices = index_gen(m,n,nmax) 44 | 45 | # retrieve iterations variable set 46 | th = tf.slice(theta, tf.constant([i]), size=tf.constant([1])) 47 | ph = tf.slice(phi, tf.constant([i]), size=tf.constant([1])) 48 | 49 | 50 | # construct beam splitter entries with compliant datatypes 51 | # a = tf.math.exp(tf.complex(0.0, ph))*tf.complex(tf.cos(th), 0.0) 52 | # b = tf.complex(-tf.sin(th), 0.0) 53 | # c = tf.math.exp(tf.complex(0.0, ph))*tf.complex(tf.sin(th), 0.0) 54 | # d = tf.complex(tf.cos(th), 0.0) 55 | 56 | # Valeria decomposition 57 | a = tf.math.exp(tf.complex(0.0, th))*tf.complex(tf.sin(ph/2), 0.0) 58 | b = tf.math.exp(tf.complex(0.0, th))*tf.complex(tf.cos(ph/2), 0.0) 59 | c = tf.complex(tf.cos(ph/2), 0.0) 60 | d = tf.complex(-tf.sin(ph/2), 0.0) 61 | 62 | # concatenate matrix elements into vector for scatter operation 63 | var_list = [tf.constant([1.0+0.0j], dtype=tf.complex64,shape=[1,])]*(2+nmax) 64 | var_list[:4] = [a, b, c, d] 65 | var_list = tf.stack(var_list, 0, name="varlist_{}".format(i)) 66 | # place variables with appropriate functionals (see Clements paper for ij=mn variable maps) 67 | Tmn = tf.scatter_nd(indices, var_list, tf.constant([nmax**2,1], dtype=tf.int64)) 68 | # reshape into rank 2 tensor 69 | 70 | # cannot use update as the variable reference does not seem to like gradient computation 71 | #Tmn = tf.scatter_nd_add(tf.zeros((nmax**2,), dtype=tf.complex64), indices, var_list, name="scatter_{}".format(i)) 72 | Tmn = tf.reshape(Tmn, tf.constant([nmax]*2)) 73 | 74 | # return unitary using specified order convention 75 | U = tf.cond(rev, lambda: tf.matmul(U, Tmn), lambda: tf.matmul(Tmn, U)) 76 | 77 | return U 78 | 79 | 80 | def index_gen(m,n,nmax): 81 | """ 82 | Generates index pair mappings for scatter update 83 | """ 84 | rows,cols= [m, m, n, n], [m, n, m, n] 85 | for i in range(nmax): 86 | # skip values that will be covered by beam splitter 87 | if (i == m) or (i==n): continue 88 | 89 | rows.append(i) 90 | cols.append(i) 91 | 92 | # comute index mappings when reshaped to rank one tensor 93 | indices = np.ravel_multi_index([rows, cols], (nmax, nmax)).reshape(nmax+2, 1).tolist() 94 | return tf.constant(indices, dtype=tf.int64) 95 | 96 | 97 | def T(m, n, theta, phi, nmax): 98 | r"""The Clements T matrix from Eq. 1 of Clements et al. (2016)""" 99 | mat = np.identity(nmax, dtype=np.complex128) 100 | # mat[m, m] = np.exp(1j*phi)*np.cos(theta) 101 | # mat[m, n] = -np.sin(theta) 102 | # mat[n, m] = np.exp(1j*phi)*np.sin(theta) 103 | # mat[n, n] = np.cos(theta) 104 | mat[m, m] = np.exp(1j*theta)*np.sin(phi/2) 105 | mat[m, n] = np.exp(1j*theta)*np.cos(phi/2) 106 | mat[n, m] = np.cos(phi/2) 107 | mat[n, n] = -np.sin(phi/2) 108 | return mat 109 | 110 | 111 | def tfT_batch(bms_spec, theta, phi): 112 | r"""The Clements T matrix with tensorflow compliance. 113 | -Most importantly works on batches 114 | """ 115 | # from bms_spec, extract m,n and nmax 116 | m = [spec[0] for spec in bms_spec] 117 | n = [spec[1] for spec in bms_spec] 118 | nmax = bms_spec[0][-1] 119 | 120 | mat_mm = tf.multiply(tf.exp(tf.complex(0.0, phi)),tf.complex(tf.cos(theta), 0.0)) 121 | 122 | mat_mn = -tf.complex(tf.sin(theta), 0.0) 123 | 124 | mat_nm = tf.multiply(tf.exp(tf.complex(0.0, phi)),tf.complex(tf.sin(theta), 0.0)) 125 | 126 | mat_nn = tf.complex(tf.cos(theta), 0.0) 127 | 128 | 129 | 130 | return mat_mm 131 | 132 | 133 | def Ti(m, n, theta, phi, nmax): 134 | r"""The inverse Clements T matrix""" 135 | return np.transpose(T(m, n, theta, -phi, nmax)) 136 | 137 | 138 | def nullTi(m, n, U): 139 | r"""Nullifies element m,n of U using Ti""" 140 | (nmax, mmax) = U.shape 141 | 142 | if nmax != mmax: 143 | raise ValueError("U must be a square matrix") 144 | 145 | if U[m, n+1] == 0: 146 | thetar = np.pi/2 147 | phir = 0 148 | else: 149 | r = U[m, n] / U[m, n+1] 150 | thetar = np.arctan(np.abs(r)) 151 | phir = np.angle(r) 152 | 153 | return [n, n+1, thetar, phir, nmax] 154 | 155 | 156 | def nullT(n, m, U): 157 | r"""Nullifies element n,m of U using T""" 158 | (nmax, mmax) = U.shape 159 | 160 | if nmax != mmax: 161 | raise ValueError("U must be a square matrix") 162 | 163 | if U[n-1, m] == 0: 164 | thetar = np.pi/2 165 | phir = 0 166 | else: 167 | r = -U[n, m] / U[n-1, m] 168 | thetar = np.arctan(np.abs(r)) 169 | phir = np.angle(r) 170 | 171 | return [n-1, n, thetar, phir, nmax] 172 | 173 | 174 | def clements(V, tol=1e-11): 175 | r"""Clements decomposition of a unitary matrix, with local 176 | phase shifts applied between two interferometers. 177 | See :ref:`clements` or :cite:`clements2016` for more details. 178 | This function returns a circuit corresponding to an intermediate step in 179 | Clements decomposition as described in Eq. 4 of the article. In this form, 180 | the circuit comprises some T matrices (as in Eq. 1), then phases on all modes, 181 | and more T matrices. 182 | The procedure to construct these matrices is detailed in the supplementary 183 | material of the article. 184 | Args: 185 | V (array[complex]): unitary matrix of size n_size 186 | tol (float): the tolerance used when checking if the matrix is unitary: 187 | :math:`|VV^\dagger-I| \leq` tol 188 | Returns: 189 | tuple[array]: tuple of the form ``(tilist,tlist,np.diag(localV))`` 190 | where: 191 | * ``tilist``: list containing ``[n,m,theta,phi,n_size]`` of the Ti unitaries needed 192 | * ``tlist``: list containing ``[n,m,theta,phi,n_size]`` of the T unitaries needed 193 | * ``localV``: Diagonal unitary sitting sandwiched by Ti's and the T's 194 | """ 195 | localV = V 196 | (nsize, _) = localV.shape 197 | 198 | diffn = np.linalg.norm(V @ V.conj().T - np.identity(nsize)) 199 | if diffn >= tol: 200 | raise ValueError("The input matrix is not unitary") 201 | 202 | tilist = [] 203 | tlist = [] 204 | for k, i in enumerate(range(nsize-2, -1, -1)): 205 | if k % 2 == 0: 206 | for j in reversed(range(nsize-1-i)): 207 | tilist.append(nullTi(i+j+1, j, localV)) 208 | localV = localV @ Ti(*tilist[-1]) 209 | else: 210 | for j in range(nsize-1-i): 211 | tlist.append(nullT(i+j+1, j, localV)) 212 | localV = T(*tlist[-1]) @ localV 213 | 214 | return tilist, tlist, np.diag(localV) 215 | 216 | 217 | def clements_phase_end(V, tol=1e-11): 218 | r"""Clements decomposition of a unitary matrix. 219 | See :cite:`clements2016` for more details. 220 | Final step in the decomposition of a given discrete unitary matrix. 221 | The output is of the form given in Eq. 5. 222 | Args: 223 | V (array[complex]): unitary matrix of size n_size 224 | tol (float): the tolerance used when checking if the matrix is unitary: 225 | :math:`|VV^\dagger-I| \leq` tol 226 | Returns: 227 | tuple[array]: returns a tuple of the form ``(tlist,np.diag(localV))`` 228 | where: 229 | * ``tlist``: list containing ``[n,m,theta,phi,n_size]`` of the T unitaries needed 230 | * ``localV``: Diagonal unitary matrix to be applied at the end of circuit 231 | """ 232 | tilist, tlist, diags = clements(V, tol) 233 | new_tlist, new_diags = tilist.copy(), diags.copy() 234 | 235 | # Push each beamsplitter through the diagonal unitary 236 | for i in reversed(tlist): 237 | em, en = int(i[0]), int(i[1]) 238 | alpha, beta = np.angle(new_diags[em]), np.angle(new_diags[en]) 239 | theta, phi = i[2], i[3] 240 | 241 | # The new parameters required for D',T' st. T^(-1)D = D'T' 242 | new_theta = theta 243 | new_phi = np.fmod((alpha - beta + np.pi), 2*np.pi) 244 | new_alpha = beta - phi + np.pi 245 | new_beta = beta 246 | 247 | new_i = [i[0], i[1], new_theta, new_phi, i[4]] 248 | new_diags[em], new_diags[en] = np.exp( 249 | 1j*new_alpha), np.exp(1j*new_beta) 250 | 251 | new_tlist = new_tlist + [new_i] 252 | 253 | return (new_tlist, new_diags) 254 | 255 | 256 | def clements_stitch(tlist, diags): 257 | """ 258 | Computes the unitary given by a clements decomposition 259 | """ 260 | # construct adjoint of the phase shifts 261 | U = np.diag(np.conjugate(diags)) 262 | # iterate over specified beam splitter arrays 263 | for Tmn in tlist[::-1]: 264 | # construct beamsplitter given parameters 265 | bm = T(*Tmn) 266 | U = U @ bm 267 | return U 268 | 269 | def str_base(num, base, length, numerals = '0123456789'): 270 | if base < 2 or base > len(numerals): 271 | raise ValueError("str_base: base must be between 2 and %i" % len(numerals)) 272 | 273 | if num == 0: 274 | return '0'*length 275 | 276 | if num < 0: 277 | sign = '-' 278 | num = -num 279 | else: 280 | sign = '' 281 | 282 | result = '' 283 | while num: 284 | result = numerals[num % (base)] + result 285 | num //= base 286 | 287 | 288 | out = sign + result 289 | 290 | if len(out) < length: 291 | out = '0'*(length - len(out)) + out 292 | return out 293 | 294 | 295 | def opto_gen(modes, targets): 296 | """ 297 | Gives a Mach-Zedner Interferometer decomposition for an arbitrary number of modes and mode 298 | target pairs. 299 | """ 300 | 301 | # initliase specification container 302 | bms_spec = [] 303 | 304 | # for each target pair, generate a template decomp using standard functionality 305 | for pair in targets: 306 | 307 | # generate MZI template 308 | template = [0,1,0,0,modes] 309 | 310 | # assume indexing starts at 1, update values explicitly 311 | template[0] = pair[0]-1 312 | template[1] = pair[1]-1 313 | 314 | # add element to blueprint list 315 | bms_spec.append(template) 316 | 317 | return bms_spec 318 | 319 | 320 | 321 | 322 | def symmetric_map(m_num, p_num): 323 | """ 324 | Computes the permutation matrix to map to the isomorphic Hilbert space for p_num photons 325 | and m_num modes, eliminating degenerate degrees of freedom. Exponentially faster than matrix permanent method 326 | everyone else insists on using but at the cost of higher memory complexity. Should probably tell 327 | someone about this... 328 | """ 329 | 330 | # compute size of output matrix 331 | row_num = comb(m_num + p_num-1, p_num); 332 | col_num = m_num**p_num; 333 | 334 | # compute photon states as an m-ary number of p_num bits 335 | photon_state = np.asarray([list(str_base(n,m_num,p_num)) for n in range(m_num**p_num)]).astype(int) 336 | 337 | # compute mode occupation number for each row 338 | fock = np.zeros((col_num, m_num), dtype=np.int32); 339 | # iterate over rows 340 | for i in range(np.shape(photon_state)[0]): 341 | # iterate over columns 342 | for j in range(m_num): 343 | fock[i,j] = np.sum(photon_state[i,:]==j); 344 | 345 | # # compute unique Fock states 346 | uniques = np.fliplr(np.unique(fock, axis=0)) 347 | ldim = np.shape(uniques)[0] 348 | 349 | 350 | # preallocate symmetric transform matrix 351 | P = np.zeros((ldim, col_num)); 352 | 353 | # iterate over symmetric dimension 354 | for k in range(ldim): 355 | for m in range(col_num): 356 | if (uniques[k,:] == fock[m,:]).all(): 357 | P[k,m] = 1; 358 | 359 | 360 | # ensure normalisation property holds 361 | P[k,:] /= np.sqrt(np.sum(P[k,:])) 362 | 363 | return P 364 | 365 | 366 | def number_states(m_num,p_num): 367 | """ 368 | outputs a list of the number states in each mode 369 | """ 370 | 371 | # compute size of output matrix 372 | row_num = comb(m_num + p_num-1, p_num); 373 | col_num = m_num**p_num; 374 | 375 | # compute photon states as an m-ary number of p_num bits 376 | photon_state = np.asarray([list(str_base(n,m_num,p_num)) for n in range(m_num**p_num)]).astype(int) 377 | 378 | # compute mode occupation number for each row 379 | fock = np.zeros((col_num, m_num), dtype=np.int32); 380 | # iterate over rows 381 | for i in range(np.shape(photon_state)[0]): 382 | # iterate over columns 383 | for j in range(m_num): 384 | fock[i,j] = np.sum(photon_state[i,:]==j); 385 | 386 | # compute unique Fock states 387 | uniques = np.fliplr(np.unique(fock, axis=0)) 388 | return uniques 389 | 390 | 391 | def nl_gen(nldesc, convert=False): 392 | """ 393 | Generates a non-linear operation and ancillary information given description 394 | 395 | nldesc := {type=[swap, sswap, pphase], u_prob = [0,1], targets=[0...1,1,0..,0]} 396 | """ 397 | 398 | if type(nldesc) is not dict: 399 | raise TypeError("Non-linear layer description must be dictionary of parameters") 400 | 401 | # extract general success probability of non linear activation 402 | u_prob = tf.constant(nldesc['u_prob'], dtype=tf.complex64) 403 | 404 | # compute dimension of system 405 | modes = nldesc["modes"] 406 | photons = nldesc["photons"] 407 | dim = comb(modes+photons-1, photons, exact=True) 408 | 409 | 410 | try: 411 | # this type is a bit more complicated, will come back to implement later 412 | if nldesc['type']=="pphase": 413 | # explicitly extract mode number and photon number 414 | m_num,p_num = len(nldesc['targets']), nldesc['photons'] 415 | # compute number state descriptor 416 | nstates = number_states(m_num, p_num) 417 | # compute nonlinear unitary 418 | unitary = np.eye(comb(m_num+p_num-1, p_num, exact=True), dtype=np.complex128) 419 | for i in range(np.shape(nstates)[0]): 420 | 421 | # multiply by mask to remove nonlinear term on those modes 422 | fock_nums = nldesc['targets']*nstates[i,:] 423 | # compute phase factor nonlinear layer 424 | unitary[i,i] *= np.prod(np.exp(1j*np.pi*(np.clip(fock_nums-1,0,None)))) 425 | 426 | elif nldesc['type']=='swap': 427 | # compute single photon unitary corresponding to requestion nonlinearity 428 | unitary = dirsum([np.asarray([1.0]), op['swap']], nldesc['targets']) 429 | 430 | # sqrt of swap gate unitary applied between arbitrary modes 431 | elif nldesc['type']=='sswap': 432 | # get target pairs 433 | pairs = nldesc['pairs'] 434 | # generate swap gate on given pairs 435 | U = np.eye(dim, dtype=np.complex128) 436 | # construct sqrt swap gate on given targets 437 | for pair in pairs: 438 | V = swap_gen(modes, photons, pair, partial=True) 439 | U = V @ U 440 | 441 | unitary = U 442 | 443 | elif nldesc['type']=='cx': 444 | unitary = dirsum([np.asarray([1.0]), op['cx']], nldesc['targets']) 445 | # generate random unitary of specified dimension - easy nonlinearity 446 | elif nldesc["type"]=="rand": 447 | # set seed if parsed 448 | if 'seed' in nldesc: 449 | np.random.seed(nldesc['seed']) 450 | # construct random unitary on specified 451 | unitary = dirsum([np.asarray([1.0]), randU(nldesc['dim'])], nldesc['targets']) 452 | 453 | elif nldesc["type"]=="id": 454 | # simply perform identity operation (useful for noisy channels) 455 | unitary = np.eye(dim) 456 | else: 457 | # non-recognised gate parameter 458 | raise ValueError("Nonlinear type \'{}\' not recognised".format(nldesc['type'])) 459 | 460 | # raise error if missing a keyword argument 461 | except KeyError as e: 462 | raise KeyError("Non-linear layer specification is missing argument {}".format(e)) 463 | 464 | # convert to 3 dimensional tensor if requested 465 | if convert: 466 | unitary = tf.convert_to_tensor(unitary, dtype=tf.complex64) 467 | 468 | return nldesc["type"], nldesc["u_prob"], unitary 469 | 470 | def swap_gen(modes, photons, pair, partial=True): 471 | """ 472 | Constructs the partial swap on the full photonic output space 473 | """ 474 | 475 | # dimension of system 476 | dim = comb(modes+photons-1, photons, exact=True) 477 | # basis 478 | basis = np.eye(dim) 479 | # number states 480 | nstates = number_states(modes,photons) 481 | 482 | # construct the unitary transform based on required mappings 483 | U = np.zeros((dim,dim), dtype=np.complex128) 484 | 485 | # iterate over all combinations of states (yuck code) 486 | for i,start_state in enumerate(nstates): 487 | # compute the swapped state 488 | swap_state = list(start_state) 489 | swap_state[pair[0]-1],swap_state[pair[1]-1] = swap_state[pair[1]-1],swap_state[pair[0]-1] 490 | 491 | for j,end_state in enumerate(nstates): 492 | if (swap_state==end_state).all(): 493 | U[i,j] = 1.0 494 | break 495 | 496 | if partial: 497 | U = scipy.linalg.sqrtm(U) 498 | return U 499 | 500 | 501 | 502 | 503 | 504 | 505 | def randU(dim): 506 | """ 507 | Generate random unitary matrices of dimension D 508 | """ 509 | 510 | X = (np.random.randn(dim,dim) + 1j*np.random.randn(dim,dim))/np.sqrt(2); 511 | Q,R = np.linalg.qr(X); 512 | R = np.diag(np.divide(np.diag(R),np.abs(np.diag(R)))); 513 | U = Q @ R; 514 | return U 515 | 516 | 517 | def bellgen(num=1): 518 | """ 519 | Generates an optical bell state on 4 modes 520 | """ 521 | 522 | # prelims 523 | nstates = number_states(4,2) 524 | dim = comb(4+2-1,2, exact=True) 525 | basis = np.eye(dim) 526 | proj = np.zeros([dim,]) 527 | 528 | # generate psi- 529 | if num==2: 530 | # basis states |1001> and -|0110> 531 | s1 = np.asarray([1, 0, 0, 1]) 532 | s2 = np.asarray([0, 1, 1, 0]) 533 | 534 | # this is bad even for me 535 | for i in range(len(nstates)): 536 | if (s1 == nstates[i,:]).all(): 537 | proj = proj + basis[:,i] 538 | 539 | if (s2 == nstates[i,:]).all(): 540 | proj = proj - basis[:,i] 541 | 542 | 543 | # generate phi+ 544 | elif num==3: 545 | # states |1010> and |0101> 546 | s1 = np.asarray([1, 0, 1, 0]) 547 | s2 = np.asarray([0, 1, 0, 1]) 548 | 549 | for i in range(len(nstates)): 550 | if (s1 == nstates[i,:]).all(): 551 | proj = proj + basis[:,i] 552 | if (s2 == nstates[i,:]).all(): 553 | proj = proj + basis[:,i] 554 | 555 | # generate phi- 556 | elif num==4: 557 | # states |1010> and -|0101> 558 | s1 = np.asarray([1, 0, 1, 0]) 559 | s2 = np.asarray([0, 1, 0, 1]) 560 | 561 | for i in range(len(nstates)): 562 | if (s1 == nstates[i,:]).all(): 563 | proj = proj + basis[:,i] 564 | 565 | if (s2 == nstates[i,:]).all(): 566 | proj = proj - basis[:,i] 567 | 568 | # default to psi+ 569 | else: 570 | # basis states |1001> and |0110> 571 | s1 = np.asarray([1, 0, 0, 1]) 572 | s2 = np.asarray([0, 1, 1, 0]) 573 | for i in range(len(nstates)): 574 | if (s1 == nstates[i,:]).all(): 575 | proj = proj + basis[:,i] 576 | 577 | if (s2 == nstates[i,:]).all(): 578 | proj = proj + basis[:,i] 579 | 580 | proj = proj.reshape(dim,1) 581 | return np.kron(proj, dagger(proj))/2 582 | 583 | 584 | def noon_gen(modes, photons, N=None): 585 | """ 586 | Generates an NOON state on the last two modes 587 | """ 588 | # basic input value check 589 | if N is None: 590 | N=photons 591 | else: 592 | if N>photons: 593 | raise ValueError("NOON state {} cannot be generated with {} photons".format(N,photons)) 594 | 595 | if modes<=2 and N!=photons: 596 | raise ValueError("Mode number must be greater than two to support ancilla photons") 597 | 598 | 599 | # prelims 600 | nstates = number_states(modes,photons) 601 | dim = comb(modes+photons-1,photons, exact=True) 602 | basis = np.eye(dim) 603 | proj = np.zeros([dim,]) 604 | 605 | # basis states |1001> and |0110> 606 | if modes>2: 607 | # gross 608 | s1 = [0]*(modes-2) 609 | s2 = [0]*(modes-2) 610 | s1[0] = photons-N 611 | s2[0] = photons-N 612 | s1.append(N) 613 | s1.append(0) 614 | s2.append(0) 615 | s2.append(N) 616 | s1 = np.asarray(s1) 617 | s2 = np.asarray(s2) 618 | else: 619 | s1 = np.asarray([N, 0]) 620 | s2 = np.asarray([0, N]) 621 | 622 | # this is bad even for me 623 | for i in range(len(nstates)): 624 | if (s1 == nstates[i,:]).all(): 625 | proj = proj + basis[:,i] 626 | 627 | if (s2 == nstates[i,:]).all(): 628 | proj = proj + np.exp(1j*np.pi*N)*basis[:,i] 629 | 630 | proj = proj.reshape(dim,1) 631 | return np.kron(proj, dagger(proj))/2 632 | 633 | 634 | 635 | 636 | def povm_gen(pdesc, convert=False): 637 | """ 638 | Generate a projection operator that collapses superpositions and can purify mixed states. 639 | pdesc := {targets=[1,2...m], modes, photons, eff=[0,1]}. 640 | """ 641 | 642 | # perform some basic input parsing 643 | if type(pdesc) is not dict: 644 | raise TypeError("Projection layer description must be dictionary of parameters") 645 | 646 | try: 647 | # get explicit layer parameters 648 | modes = pdesc["modes"] 649 | photons = pdesc["photons"] 650 | targets = pdesc["targets"] 651 | 652 | # generate null mask for target modes 653 | null = np.asarray([1.0 if i in targets else 0.0 for i in range(1,modes+1)]) 654 | 655 | dim = comb(modes+photons-1, photons, exact=True) 656 | # compute number states 657 | nstates = number_states(modes, photons) 658 | # generate a basis set to draw from 659 | basis = np.eye(dim) 660 | # for each measurement outcome on designated modes, compute projector 661 | projectors = [] 662 | complete = [] 663 | for i in range(dim): 664 | # get outcome target state 665 | fvec = nstates[i,:] 666 | fvec = null*fvec 667 | # pick measurement outcome 668 | if list(fvec) in complete: 669 | continue 670 | else: 671 | # null free modes with elementwise multiplication 672 | complete.append(list(fvec)) 673 | 674 | 675 | # iterate over number states and find correct mappings 676 | M = np.zeros((dim,dim)) 677 | for j in range(dim): 678 | if (fvec == (null*nstates[j,:])).all(): 679 | M += np.kron(basis[:,j].reshape(dim,1), basis[:,j].reshape(1,dim)) 680 | 681 | projectors.append(M) 682 | 683 | 684 | except KeyError as e: 685 | raise KeyError("Projection layer specification is missing argument {}".format(e)) 686 | 687 | # repackage into projector tensor 688 | proj = np.zeros((len(projectors), dim,dim)) 689 | for i in range(len(proj)): 690 | proj[i,:,:] = projectors[i] 691 | 692 | # convert to 3 dimensional tensor if requested 693 | if convert: 694 | proj = tf.convert_to_tensor(proj, dtype=tf.complex64) 695 | 696 | return proj 697 | 698 | 699 | def iso_gen(idesc, convert=False): 700 | """ 701 | Generates a pseudo isometry for mapping ancilla modes contents to a single 702 | state. Targets specifies the ancilla modes, dest is the ancilla 703 | idesc: = {targets=[1,2,3], modes,photons, dest=1,2,...} 704 | """ 705 | 706 | # perform some basic input parsing 707 | if type(idesc) is not dict: 708 | raise TypeError("Projection layer description must be dictionary of parameters") 709 | 710 | try: 711 | # get explicit layer parameters 712 | modes = idesc["modes"] 713 | photons = idesc["photons"] 714 | targets = idesc["targets"] 715 | dest = idesc["dest"] 716 | 717 | if dest not in targets: 718 | raise ValueError("Destination mode must be one of the specified ancilla modes") 719 | 720 | # compute system dimension 721 | dim = comb(modes+photons-1, photons, exact=True) 722 | # generate null mask for target modes 723 | null = np.asarray([1 if i in targets else 0 for i in range(1,modes+1)]) 724 | # generate null mask for non-target modes 725 | null_inv = np.asarray([0 if i in targets else 1 for i in range(1,modes+1)]) 726 | # compute number states 727 | nstates = number_states(modes, photons) 728 | # generate a basis set to draw from 729 | basis = np.eye(dim) 730 | 731 | # for each measurement outcome on designated modes, compute projector 732 | projectors = [] 733 | complete = [] 734 | for i in range(dim): 735 | # get input state to map 736 | fvec = nstates[i,:] 737 | fvec = null*fvec 738 | 739 | # pick measurement outcome 740 | if list(fvec) in complete: 741 | continue 742 | else: 743 | # null free modes with elementwise multiplication 744 | complete.append(list(fvec)) 745 | 746 | # compute number of photons in ancilla modes 747 | anc_photons = np.sum(fvec) 748 | # construct state that this should map to (all photons in dest mode) 749 | map_state = np.zeros((modes,),dtype=np.int16) 750 | map_state[dest-1] = anc_photons 751 | # cannot find basis element to map to as this depends on target modes 752 | # iterate over number states and find correct mappings 753 | 754 | M = np.zeros((dim,dim)) 755 | for j in range(dim): 756 | if (fvec == (null*nstates[j,:])).all(): 757 | # compute basis state to map from 758 | input_state = basis[:,j] 759 | # compute basis state to map to 760 | output_state = map_state+null_inv*nstates[j,:] 761 | # get basis element of mapped state 762 | ind = np.where(np.all(nstates==output_state,axis=1))[0][0] 763 | output_state = basis[:,ind] 764 | # compute projector mapping 765 | M += np.kron(output_state.reshape(dim,1), input_state.reshape(1,dim)) 766 | 767 | projectors.append(M) 768 | 769 | 770 | except KeyError as e: 771 | raise KeyError("Isometric layer specification is missing argument {}".format(e)) 772 | 773 | # repackage into projector tensor 774 | proj = np.zeros((len(projectors), dim,dim)) 775 | for i in range(len(proj)): 776 | proj[i,:,:] = projectors[i] 777 | 778 | # convert to 3 dimensional tensor if requested 779 | if convert: 780 | proj = tf.convert_to_tensor(proj, dtype=tf.complex64) 781 | 782 | return proj 783 | 784 | 785 | def Udata_gen(U, num=1000): 786 | """ 787 | Generates some input/output data for unitary evolution 788 | """ 789 | # compute dimension of system 790 | dim = np.size(U, 1) 791 | 792 | # generate randomly sampled pure input states 793 | psi = haar_sample(dim=dim, num=num, pure=True, operator=True) 794 | 795 | # preallocate unitary output states 796 | phi = np.zeros_like(psi) 797 | 798 | # compute output states phi subject to U*psi*U' 799 | for i in range(num): 800 | phi[i, :, :] = U @ psi[i, :, :] @ dagger(U) 801 | 802 | psi = tf.convert_to_tensor(psi, dtype=tf.complex64, name='psi') 803 | phi = tf.convert_to_tensor(phi, dtype=tf.complex64, name='phi') 804 | 805 | return psi, phi 806 | 807 | def M_apply(M,rhob): 808 | """ 809 | Applies a map M to an input batch of states 810 | """ 811 | for i in range(np.shape(rhob)[0]): 812 | rho = np.asarray(rhob[i,:,:]) 813 | rhob[i,:,:] *= 0 814 | for j in range(np.shape(M)[0]): 815 | rho += M[j,:,:] @ rho @ dagger(M[j,:,:]) 816 | rhob[i,:,:] = rho 817 | 818 | return rhob 819 | 820 | 821 | def keraskol(rho, gamma): 822 | """ 823 | tensorflow compatible quantum kolmogorov distance 824 | """ 825 | return tf.linalg.trace(tf.abs(rho-gamma)) 826 | 827 | 828 | def purekol(rho, gamma): 829 | """ 830 | Computes a mixed metric of trace distance and purity 831 | """ 832 | purity = tf.abs(tf.linalg.trace(tf.einsum('bjk,bkl->bjl', gamma, gamma))) 833 | kol = keraskol(rho, gamma) 834 | return tf.divide(kol, purity) 835 | 836 | 837 | def kerasfid(rho, gamma): 838 | """ 839 | keras compatible quantum fidelity as a minimisation task 840 | """ 841 | print(rho) 842 | return 1 - tf.real(tf.linalg.trace(tf.matmul(rho, gamma))) 843 | 844 | 845 | def mean_error(rho, gamma): 846 | """ 847 | tensorflow mean error 848 | """ 849 | return tf.abs(tf.metrics.mean(rho-gamma)) 850 | 851 | 852 | def lrscheduler(epoch): 853 | """ 854 | Scheduler callback function for learning rate 855 | """ 856 | lrate = 1e-3/((1+epoch)/100) 857 | 858 | return lrate 859 | 860 | def null_matrix(modes, photons, convert=False): 861 | """ 862 | computes null matrix for loss calcuation updates by penalising 863 | indistingushable states for NOON states 864 | """ 865 | 866 | # compute dimension of system 867 | dim = comb(modes+photons-1, photons, exact=True) 868 | null = np.zeros((dim,dim)) 869 | nstates = number_states(modes, photons) 870 | basis = np.eye(dim) 871 | 872 | # iterate over dimension of space 873 | for i in range(dim): 874 | for j in range(dim): 875 | # get index element states 876 | avec = nstates[i,:] 877 | bvec = nstates[j,:] 878 | # check if either are bad and weight appropriately 879 | if np.sum(avec[-2:])==photons: 880 | if np.prod(avec[-2:])>=0 and np.sum(avec[:-2])==0: 881 | null[i,j] = 1.0 882 | 883 | if np.sum(bvec[-2:])==photons: 884 | if np.prod(bvec[-2:])>=0 and np.sum(bvec[:-2])==0: 885 | null[i,j] = 1.0 886 | if convert: 887 | null = tf.convert_to_tensor(null, dtype=tf.complex64) 888 | 889 | return null 890 | 891 | def weight_matrix(modes, photons, w=5, convert=False): 892 | """ 893 | computes weight matrix for loss calcuation updates by penalising 894 | indistingushable states for NOON states 895 | """ 896 | 897 | # compute dimension of system 898 | dim = comb(modes+photons-1, photons, exact=True) 899 | weights = np.ones((dim,dim)) 900 | nstates = number_states(modes, photons) 901 | basis = np.eye(dim) 902 | 903 | # iterate over dimension of space 904 | for i in range(dim): 905 | for j in range(dim): 906 | # get index element states 907 | avec = nstates[i,:] 908 | bvec = nstates[j,:] 909 | # check if either are bad and weight appropriately 910 | if np.sum(avec[-2:])==photons: 911 | if np.prod(avec[-2:])>0 and np.sum(avec[:-2])==0: 912 | weights[i,j] *= w 913 | 914 | if np.sum(bvec[-2:])==photons: 915 | if np.prod(bvec[-2:])>0 and np.sum(bvec[:-2])==0: 916 | weights[i,j] *= w 917 | if convert: 918 | weights = tf.convert_to_tensor(weights, dtype=tf.complex64) 919 | 920 | return weights 921 | 922 | 923 | def wkfid(weights): 924 | """ 925 | Computes quantum keras fidelity with weights matrix 926 | """ 927 | def weightfid(rho, gamma): 928 | """ 929 | tensorflow compatible quantum kolmogorov distance 930 | """ 931 | gam = tf.math.multiply(weights,gamma) 932 | return 1 - tf.real(tf.linalg.trace(tf.matmul(rho,gam))) 933 | 934 | return weightfid 935 | 936 | def probfid(null): 937 | """ 938 | Computes quantum keras fidelity with weights matrix 939 | """ 940 | def combi(rho, gamma): 941 | """ 942 | tensorflow compatible quantum kolmogorov distance 943 | """ 944 | gam = tf.multiply(null,gamma) 945 | #gam = tf.divide(gam, tf.linalg.trace(gam)) 946 | return 1 - tf.real(tf.linalg.trace(tf.matmul(rho,gamma)))*tf.math.pow(tf.real(tf.linalg.trace(tf.matmul(rho,gam))), tf.constant(2.0)) 947 | 948 | return combi 949 | 950 | def bell_gen(modes, photons, bell=1): 951 | """ 952 | Generates a bell state of two photons using the last 4 modes 953 | """ 954 | 955 | assert modes>= 4, "Need at least 4 modes for Bell state generation" 956 | assert photons >=2, "Need at least two photons for Bell state generation" 957 | 958 | 959 | # number of ancilla photons 960 | aphotons = photons - 2 961 | # number of ancilla modes 962 | amodes = modes - 4 963 | if amodes == 0 and photons > 2: 964 | aphotons = 0 965 | photons = 2 966 | print("Warning: Must have ancilla modes for ancilla photons, truncating") 967 | 968 | # prelims 969 | nstates = number_states(modes,photons) 970 | # dimension of space 971 | dim = comb(modes+photons-1,photons, exact=True) 972 | # basis set for complete space 973 | basis = np.eye(dim) 974 | # output projector 975 | proj = np.zeros([dim,]) 976 | 977 | # ancilla output state 978 | if amodes>0: 979 | aout = [0]*amodes 980 | aout[0] = aphotons 981 | else: 982 | aout = [] 983 | 984 | # generate psi- 985 | if bell==2: 986 | # basis states |1001> and -|0110> 987 | s1 = aout + [1, 0, 0, 1] 988 | s2 = aout + [0, 1, 1, 0] 989 | 990 | 991 | # this is bad even for me 992 | for i in range(dim): 993 | if (s1 == nstates[i,:]).all(): 994 | proj = proj + basis[:,i] 995 | 996 | if (s2 == nstates[i,:]).all(): 997 | proj = proj - basis[:,i] 998 | 999 | 1000 | # generate phi+ 1001 | elif bell==3: 1002 | # states |1010> and |0101> 1003 | s1 = aout + [1, 0, 1, 0] 1004 | s2 = aout + [0, 1, 0, 1] 1005 | 1006 | for i in range(dim): 1007 | if (s1 == nstates[i,:]).all(): 1008 | proj = proj + basis[:,i] 1009 | 1010 | if (s2 == nstates[i,:]).all(): 1011 | proj = proj + basis[:,i] 1012 | 1013 | # generate phi- 1014 | elif bell==4: 1015 | 1016 | # states |1010> and -|0101> 1017 | s1 = aout + [1, 0, 1, 0] 1018 | s2 = aout + [0, 1, 0, 1] 1019 | 1020 | for i in range(dim): 1021 | if (s1 == nstates[i,:]).all(): 1022 | proj = proj + basis[:,i] 1023 | 1024 | if (s2 == nstates[i,:]).all(): 1025 | proj = proj - basis[:,i] 1026 | 1027 | 1028 | # default to psi+ 1029 | else: 1030 | # basis states |1001> and |0110> 1031 | s1 = aout + [1, 0, 0, 1] 1032 | s2 = aout + [0, 1, 1, 0] 1033 | 1034 | for i in range(dim): 1035 | if (s1 == nstates[i,:]).all(): 1036 | proj = proj + basis[:,i] 1037 | 1038 | if (s2 == nstates[i,:]).all(): 1039 | proj = proj + basis[:,i] 1040 | 1041 | proj = proj.reshape(dim,1) 1042 | return np.kron(proj, dagger(proj))/2 1043 | 1044 | def proj_gen(outcome): 1045 | """ 1046 | Generates the measurement projector for specified Fock state. 1047 | """ 1048 | 1049 | # input parsing 1050 | modes = len(outcome) 1051 | photons = sum(outcome) 1052 | 1053 | # dimension 1054 | dim = dim = comb(modes+photons-1, photons, exact=True) 1055 | # compute number states 1056 | nstates = number_states(modes, photons) 1057 | # basis set for complete space 1058 | basis = np.eye(dim) 1059 | # output projector 1060 | proj = np.zeros([dim,]) 1061 | 1062 | # find basis element 1063 | for i in range(dim): 1064 | if (outcome == nstates[i,:]).all(): 1065 | proj = proj + basis[:,i] 1066 | 1067 | proj = proj.reshape(dim,1) 1068 | return np.kron(proj, dagger(proj)) 1069 | 1070 | 1071 | def bell_train_gen(amodes, aphotons, convert=False): 1072 | """ 1073 | Generates the training set for bell state discrimination 1074 | """ 1075 | modes = 4 + amodes 1076 | photons = 2 + aphotons 1077 | 1078 | # ancillary outcomes 1079 | if amodes>0: 1080 | aout = [0]*amodes 1081 | aout[0] = aphotons 1082 | else: 1083 | aout = [] 1084 | 1085 | # dimension 1086 | dim = dim = comb(modes+photons-1, photons, exact=True) 1087 | 1088 | # preallocate input states out measurement projectors 1089 | bells = np.zeros((4, dim,dim), dtype=np.complex128) 1090 | projs = np.zeros((4, dim,dim), dtype=np.complex128) 1091 | 1092 | # generate each bell state and store 1093 | for i in range(4): 1094 | bells[i,:,:] = bell_gen(modes, photons, bell=i+1) 1095 | 1096 | # no clever way of doing this, hardcode each desired outcome 1097 | psip1 = aout + [1,0,0,1] 1098 | psip2 = aout + [0,1,1,0] 1099 | projs[0,:,:] = proj_gen(psip1) + proj_gen(psip2) 1100 | 1101 | psip1 = aout + [1,1,0,0] 1102 | psip2 = aout + [0,0,1,1] 1103 | projs[1,:,:] = proj_gen(psip1) + proj_gen(psip2) 1104 | 1105 | # psip1 = aout + [2,0,0,0] 1106 | # psip2 = aout + [0,2,0,0] 1107 | # projs[2,:,:] = proj_gen(psip1) + proj_gen(psip2) 1108 | 1109 | psip1 = aout + [2,0,0,0] 1110 | psip2 = aout + [0,2,0,0] 1111 | projs[2,:,:] += proj_gen(psip1) + proj_gen(psip2) 1112 | 1113 | if convert: 1114 | bells = tf.convert_to_tensor(bells, dtype=tf.complex64) 1115 | projs = tf.convert_to_tensor(projs, dtype=tf.complex64) 1116 | 1117 | return bells, projs 1118 | 1119 | 1120 | 1121 | 1122 | def fid_min(rho, gamma): 1123 | """ 1124 | Computes the fidelity of rho with itself and other members 1125 | """ 1126 | 1127 | # get shape of tensor 1128 | gamma_shape = gamma.get_shape().as_list() 1129 | 1130 | # multiplication of outputs with each other 1131 | 1132 | # iterate over fid 1133 | # fid = 0.0 1134 | # for i in range(1,4): 1135 | # # roll matrix 1136 | # gamma_roll = tf.roll(gamma, shift=i,axis=0) 1137 | # error = gamma - gamma_roll 1138 | 1139 | # abseig = tf.abs(tf.linalg.eigvalsh(error)) 1140 | 1141 | 1142 | # # compute intermediate matrices 1143 | # # emult1 = tf.einsum('ijk,ikl->ijl', gammasqrtm, gamma_roll) 1144 | # # emult2 = tf.einsum('ijk,ikl->ijl', emult1, gammasqrtm) 1145 | # # emultsqrtm = tf.linalg.sqrtm(emult2) 1146 | 1147 | # # compute fidelity measure 1148 | # fid = fid + tf.reduce_sum(abseig) 1149 | 1150 | gamma = tf.abs(gamma) 1151 | 1152 | #gamma = tf.multiply(gamma, tf.eye(num_rows=gamma_shape[-1], batch_shape=gamma_shape[0])) 1153 | fid_ten = tf.einsum('ijk,mkl->imjl', gamma, gamma) 1154 | purity = tf.einsum('ijk,ikl->ijl', gamma, gamma) 1155 | # trace on each multiplication - fidelity measure 1156 | fid_ten = tf.linalg.trace(fid_ten) 1157 | purity = tf.linalg.trace(purity) 1158 | fid = tf.real(tf.reduce_sum(fid_ten))-tf.real(tf.reduce_sum(purity)) 1159 | 1160 | return fid 1161 | 1162 | def cellkey(brain): 1163 | """ 1164 | fitness extractor for sorting purposes 1165 | """ 1166 | return brain.fitness 1167 | 1168 | def mute(): 1169 | sys.stdout = open(os.devnull, 'w') 1170 | 1171 | 1172 | if __name__ == '__main__': 1173 | print(T(1,1,0,0,2)) 1174 | 1175 | -------------------------------------------------------------------------------- /ucell/ucell.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import tensorflow as tf 6 | import math 7 | import numpy as np 8 | 9 | from scipy.special import comb 10 | from keras import optimizers 11 | from keras.layers import Input, Layer 12 | from keras.models import Model 13 | from keras import callbacks 14 | from keras import backend 15 | from datetime import datetime 16 | from qinfo.qinfo import haar_sample, dagger, multikron, dirsum 17 | from ucell.utility import * 18 | from ucell.operators import * 19 | 20 | # For the love of god, please be quiet 21 | #tf.logging.set_verbosity(tf.logging.ERROR) 22 | 23 | 24 | class ReNormaliseLayer(Layer): 25 | """ 26 | Performs a renormalisation process for a set of input variables. 27 | """ 28 | 29 | def __init__(self, dim, **kwargs): 30 | # layer identifier 31 | self.id = "renorm" 32 | # store input/output dimension 33 | self.input_dim = self.output_dim = dim 34 | # pass additional keywords to superclass initialisation 35 | super(ReNormaliseLayer, self).__init__(**kwargs) 36 | 37 | 38 | def build(self, input_shape): 39 | """Creates the variables of the layer (optional, for subclass implementers). 40 | 41 | This is a method that implements of subclasses of `Layer` or `Model` 42 | can override if they need a state-creation step in-between 43 | layer instantiation and layer call. 44 | 45 | This is typically used to create the weights of `Layer` subclasses. 46 | 47 | Arguments: 48 | input_shape: Instance of `TensorShape`, or list of instances of 49 | `TensorShape` if the layer expects a list of inputs 50 | (one instance per input). 51 | """ 52 | 53 | # call build method of super class 54 | super(ReNormaliseLayer, self).build(input_shape) 55 | 56 | def call(self, inputs): 57 | """ 58 | This is where the layer's logic lives. 59 | Arguments: 60 | inputs: Input tensor, or list/tuple of input tensors. 61 | **kwargs: Additional keyword arguments. 62 | 63 | Returns: 64 | A tensor or list/tuple of tensors. 65 | """ 66 | # extract input tensors 67 | u_params = inputs[0] 68 | norms = inputs[1] 69 | 70 | # roll back normalisation effect done in preprocessing 71 | out = tf.einsum('ij,i->ij', u_params, norms) 72 | 73 | return out 74 | 75 | def compute_output_shape(self, input_shape): 76 | assert isinstance(input_shape, list) 77 | shape_a, shape_b = input_shape 78 | return [(shape_a[0], self.output_dim), shape_b[:-1]] 79 | 80 | def get_config(self): 81 | base_config = super(ReNormaliseLayer, self).get_config() 82 | base_config['output_dim'] = self.output_dim 83 | return base_config 84 | 85 | @classmethod 86 | def from_config(cls, config): 87 | return cls(**config) 88 | 89 | @property 90 | def output_size(self): 91 | return [self.output_dim, self.output_dim] 92 | 93 | 94 | class ULayer(Layer): 95 | """ 96 | Subclass Keras because I'm a busy guy. Untitary layer using Clements 2016 decomposition, universal 97 | for single photon input. 98 | """ 99 | # initialise as subclass of keras general layer description 100 | def __init__(self, modes, photons=1, dim=None, u_noise=None, pad=0, vec=False, full=False, force=False, **kwargs): 101 | # layer identifier 102 | self.id = "unitary" 103 | # pad dimensions 104 | self.pad = pad 105 | # number of modes and number of pad modes 106 | self.modes = modes + self.pad 107 | # number of variables in operator 108 | self.vars = (self.modes**2 - self.modes)//2 109 | # number of photons 110 | self.photons = photons 111 | # whether to implement on full finite Fock space 112 | self.full = full # not currently used 113 | # pad modes - becomes dimension with 1 photon which we generally desire for universality 114 | self.pad = pad 115 | # whether to expect vector inputs 116 | self.vec = tf.constant(vec) 117 | # emergent dimension 118 | self.input_dim = self.output_dim = comb(self.modes+self.photons-1, self.photons, exact=True) + self.pad 119 | # catch extreme cases 120 | if self.input_dim > 100 and not force: 121 | raise ValueError( 122 | "System dimension is large ({}), decrease system dimension or set force flag to True".format(self.input_dim)) 123 | 124 | # keep local copy of beam splitter decomposition table 125 | # TODO: This is such a cop out 126 | self.bms_spec = clements_phase_end(np.eye(self.modes))[0] 127 | 128 | # pass additional keywords to superclass initialisation 129 | super(ULayer, self).__init__(**kwargs) 130 | 131 | 132 | def build(self, input_shape): 133 | """Creates the variables of the layer (optional, for subclass implementers). 134 | 135 | This is a method that implements of subclasses of `Layer` or `Model` 136 | can override if they need a state-creation step in-between 137 | layer instantiation and layer call. 138 | 139 | This is typically used to create the weights of `Layer` subclasses. 140 | 141 | Arguments: 142 | input_shape: Instance of `TensorShape`, or list of instances of 143 | `TensorShape` if the layer expects a list of inputs 144 | (one instance per input). 145 | """ 146 | 147 | # define weight initialisers with very tight distribution - corresponds to an identity 148 | with tf.init_scope(): 149 | diag_init = tf.initializers.RandomNormal(mean=0, stddev=0.01) 150 | theta_init = tf.initializers.RandomNormal(mean=np.pi, stddev=0.01) 151 | phi_init = tf.initializers.RandomNormal(mean=-np.pi, stddev=0.01) 152 | 153 | # superclass method for variable construction, get_variable has trouble with model awareness 154 | self.diag = self.add_weight(name="diags", 155 | shape=[self.modes], 156 | dtype=tf.float32, 157 | initializer=diag_init, 158 | trainable=True) 159 | 160 | self.theta = self.add_weight(name='theta', 161 | shape=[self.vars], 162 | dtype=tf.float32, 163 | initializer=theta_init, 164 | trainable=True) 165 | 166 | self.phi = self.add_weight(name='phi', 167 | shape=[self.vars], 168 | dtype=tf.float32, 169 | initializer=phi_init, 170 | trainable=True) 171 | 172 | # add weights to layer 173 | # construct single photon unitary 174 | self.unitary = tf_clements_stitch( 175 | self.bms_spec, self.theta, self.phi, self.diag) 176 | 177 | # construct multiphoton unitary using memory hungry (but uber fast) method 178 | if self.photons > 1: 179 | if self.full: 180 | # preallocate on zero dimensional subspace 181 | U = tf.linalg.LinearOperatorFullMatrix(tf.constant([[1.0]], dtype=tf.complex64)) 182 | 183 | for pnum in range(1,self.photons+1): 184 | # use symmetric map to compute multi photon unitary 185 | S = tf.constant(symmetric_map( 186 | self.modes, pnum), dtype=tf.complex64) 187 | # map to product state then use symmetric isometry to reduce to isomorphic subspace 188 | V = tf.matmul(S, tf.matmul(tf_multikron(self.unitary, pnum), tf.linalg.adjoint(S))) 189 | U = tf.linalg.LinearOperatorBlockDiag([U, tf.linalg.LinearOperatorFullMatrix(V)]) 190 | # convert unit 191 | self.unitary = tf.convert_to_tensor(U.to_dense()) 192 | 193 | else: 194 | # use symmetric map to compute multi photon unitary 195 | S = tf.constant(symmetric_map( 196 | self.modes, self.photons), dtype=tf.complex64) 197 | # map to product state then use symmetric isometry to reduce to isomorphic subspace 198 | self.unitary = tf.matmul(S, tf.matmul(tf_multikron(self.unitary, self.photons), tf.linalg.adjoint(S))) 199 | 200 | # adds identity channels to operator - not clear what use I thought these would be 201 | # if self.pad is not None: 202 | # # pad operator dimension as many times as requested 203 | # pad_op = [tf.constant([[1.0]], dtype=tf.complex64)]*self.pad 204 | # pad_op.append(self.unitary) 205 | 206 | # # perform block diagonal operator 207 | # linop_blocks = [tf.linalg.LinearOperatorFullMatrix(block) for block in pad_op] 208 | # self.unitary = tf.convert_to_tensor(tf.linalg.LinearOperatorBlockDiag(linop_blocks).to_dense()) 209 | 210 | # call build method of super class 211 | super(ULayer, self).build(input_shape) 212 | 213 | def call(self, inputs): 214 | """ 215 | This is where the layer's logic lives. 216 | Arguments: 217 | inputs: Input tensor, or list/tuple of input tensors. 218 | **kwargs: Additional keyword arguments. 219 | 220 | Returns: 221 | A tensor or list/tuple of tensors. 222 | """ 223 | # perform matrix calculation using multiple einsums 224 | 225 | 226 | #if self.vec: 227 | inputs = tf.complex(inputs, 0.0) 228 | out = tf.einsum('ij,bj->bi', self.unitary, 229 | inputs, name="Einsum_left") 230 | out = tf.math.real(out) 231 | # else: 232 | 233 | # leftm = tf.einsum('ij,bjl->bil', self.unitary, 234 | # inputs, name="Einsum_left") 235 | # out = tf.einsum('bil,lj->bij', leftm, 236 | # tf.linalg.adjoint(self.unitary), name="Einsum_right") 237 | return out 238 | 239 | def invert(self): 240 | """ 241 | Computes the inverse of the unitary operator 242 | """ 243 | # compute transpose of unitary 244 | self.unitary = tf.tranpose(self.unitary, conjugate=True, name="dagger_op") 245 | 246 | 247 | def compute_output_shape(self, input_shape): 248 | shape = tf.TensorShape(input_shape).as_list() 249 | shape[-1] = self.output_dim 250 | return tf.TensorShape(shape) 251 | 252 | def get_config(self): 253 | base_config = super(ULayer, self).get_config() 254 | base_config['output_dim'] = self.output_dim 255 | return base_config 256 | 257 | @classmethod 258 | def from_config(cls, config): 259 | return cls(**config) 260 | 261 | @property 262 | def output_size(self): 263 | return [self.output_dim, self.output_dim] 264 | 265 | 266 | class InvertibleLeakyReLU(Layer): 267 | """Leaky version of a Rectified Linear Unit. 268 | 269 | It allows a small gradient when the unit is not active: 270 | `f(x) = alpha * x for x < 0`, 271 | `f(x) = x for x >= 0`. 272 | 273 | # Input shape 274 | Arbitrary. Use the keyword argument `input_shape` 275 | (tuple of integers, does not include the samples axis) 276 | when using this layer as the first layer in a model. 277 | 278 | # Output shape 279 | Same shape as the input. 280 | 281 | # Arguments 282 | alpha: float >= 0. Negative slope coefficient. 283 | 284 | # References 285 | - [Rectifier Nonlinearities Improve Neural Network Acoustic Models]( 286 | https://ai.stanford.edu/~amaas/papers/relu_hybrid_icml2013_final.pdf) 287 | """ 288 | 289 | def __init__(self, alpha=0.3, **kwargs): 290 | super(InvertibleLeakyReLU, self).__init__(**kwargs) 291 | self.supports_masking = True 292 | self.alpha = K.cast_to_floatx(alpha) 293 | 294 | def call(self, inputs): 295 | return K.relu(inputs, alpha=self.alpha) 296 | 297 | def get_config(self): 298 | config = {'alpha': float(self.alpha)} 299 | base_config = super(InvertibleLeakyReLU, self).get_config() 300 | return dict(list(base_config.items()) + list(config.items())) 301 | 302 | def compute_output_shape(self, input_shape): 303 | return input_shape 304 | 305 | 306 | class UParamLayer(Layer): 307 | """ 308 | Paramterised MZI that takes progamming and input state as input 309 | 310 | targets: modes to act on, assumes and enforces sorted list pairs, indexing starting at 1 311 | 312 | Set model's input and output specs based on the input data received. 313 | 314 | This is to be used for Model subclasses, which do not know at instantiation 315 | time what their inputs look like. 316 | 317 | # Arguments 318 | inputs: Single array, or list of arrays. The arrays could be placeholders, 319 | Numpy arrays, or data tensors. 320 | - if placeholders: the model is built on top of these placeholders, 321 | and we expect Numpy data to be fed for them when calling `fit`/etc. 322 | - if Numpy data: we create placeholders matching the shape of the Numpy 323 | arrays. We expect Numpy data to be fed for these placeholders 324 | when calling `fit`/etc. 325 | - if data tensors: the model is built on top of these tensors. 326 | We do not expect any Numpy data to be provided when calling `fit`/etc. 327 | outputs: Optional output tensors (if already computed by running 328 | the model). 329 | training: Boolean or None. Only relevant in symbolic mode. Specifies 330 | whether to build the model's graph in inference mode (False), training 331 | mode (True), or using the Keras learning phase (None). 332 | """ 333 | # initialise as subclass of keras general layer description 334 | def __init__(self, modes, photons, targets, force=False, **kwargs): 335 | # layer identifier 336 | self.id = "param_unitary" 337 | # total number of modes present on chip 338 | self.modes = modes 339 | # number of single photons to be sent into system 340 | self.photons = photons 341 | # number of MZIs 342 | self.element_num = len(targets) 343 | # store MZI target modes 344 | self.targets = targets 345 | 346 | # sort mode pairs into ascending order 347 | for i,pair in enumerate(targets): 348 | # check for basic problems before getting to Tensorflow's abysmal bug reporting 349 | if max(pair)>modes: 350 | raise ValueError("One or more pair targets is greater than specified number of modes: {}>{}".format(pair,self.modes)) 351 | pair.sort() 352 | # sort and assign to target list 353 | self.targets[i] = pair 354 | 355 | # compute and store dimension of Hilbert space isomorphic to symmetric Fock subspace 356 | self.input_dim = self.output_dim = comb(self.modes+self.photons-1, self.photons, exact=True) 357 | 358 | # catch extreme cases 359 | if self.input_dim > 100 and not force: 360 | raise ValueError( 361 | "System dimension is large ({}), decrease system dimension or set force flag to True".format(self.input_dim)) 362 | 363 | # pass additional keywords to superclass initialisation 364 | super(UParamLayer, self).__init__(**kwargs) 365 | 366 | def build(self, input_shape): 367 | """Set model's input and output specs based on the input data received. 368 | 369 | This is to be used for Model subclasses, which do not know at instantiation 370 | time what their inputs look like. 371 | 372 | # Arguments 373 | inputs: Single array, or list of arrays. The arrays could be placeholders, 374 | Numpy arrays, or data tensors. 375 | - if placeholders: the model is built on top of these placeholders, 376 | and we expect Numpy data to be fed for them when calling `fit`/etc. 377 | - if Numpy data: we create placeholders matching the shape of the Numpy 378 | arrays. We expect Numpy data to be fed for these placeholders 379 | when calling `fit`/etc. 380 | - if data tensors: the model is built on top of these tensors. 381 | We do not expect any Numpy data to be provided when calling `fit`/etc. 382 | outputs: Optional output tensors (if already computed by running 383 | the model). 384 | training: Boolean or None. Only relevant in symbolic mode. Specifies 385 | whether to build the model's graph in inference mode (False), training 386 | mode (True), or using the Keras learning phase (None). 387 | """ 388 | 389 | # compute and save a blueprint of the desired optical setup 390 | self.bms_spec = opto_gen(self.modes, self.targets) 391 | 392 | #TODO: Should perform the symmetric mapping here and save that rather than reperforming it every time the 393 | # channel is called 394 | # use symmetric map to compute multi photon unitary 395 | if self.photons>1: 396 | self.S = tf.constant(symmetric_map(self.modes, self.photons), dtype=tf.complex64) 397 | self.Sadj = tf.linalg.adjoint(self.S) 398 | 399 | # call build method of super class 400 | super(UParamLayer, self).build(input_shape) 401 | 402 | def call(self, inputs): 403 | """Set model's input and output specs based on the input data received. 404 | 405 | This is to be used for Model subclasses, which do not know at instantiation 406 | time what their inputs look like. 407 | 408 | # Arguments 409 | inputs: Single array, or list of arrays. The arrays could be placeholders, 410 | Numpy arrays, or data tensors. 411 | - if placeholders: the model is built on top of these placeholders, 412 | and we expect Numpy data to be fed for them when calling `fit`/etc. 413 | - if Numpy data: we create placeholders matching the shape of the Numpy 414 | arrays. We expect Numpy data to be fed for these placeholders 415 | when calling `fit`/etc. 416 | - if data tensors: the model is built on top of these tensors. 417 | We do not expect any Numpy data to be provided when calling `fit`/etc. 418 | outputs: Optional output tensors (if already computed by running 419 | the model). 420 | training: Boolean or None. Only relevant in symbolic mode. Specifies 421 | whether to build the model's graph in inference mode (False), training 422 | mode (True), or using the Keras learning phase (None). 423 | """ 424 | 425 | # ensure input is list 426 | # if not isinstance(inputs, list): 427 | # raise TypeError("Input to UParamLayer must be list of tensors containg input states and Unitary spec, instead is {}".format(type(inputs))) 428 | 429 | # segregate inputs - why the fuck is the operator being rebuilt every time!? 430 | params = inputs[0] 431 | input_state = inputs[1] 432 | 433 | # construct batch of unitaries given optical blueprint and input parameters, parallelise on batches 434 | self.unitary = tf.map_fn(fn=self.map_clements, 435 | elems=params, 436 | dtype=tf.complex64, 437 | back_prop=True, 438 | parallel_iterations=True, 439 | name="Optical_Map") 440 | 441 | # construct multiphoton unitary using memory hungry method 442 | # if self.photons > 1: 443 | # # map to product state then use symmetric isometry to reduce to isomorphic subspace 444 | # self.unitary = tf.matmul(self.S, tf.matmul(tf_multikron( 445 | # self.unitary, self.photons), self.Sadj)) 446 | 447 | # perform matrix calculation using multiple einsums 448 | leftm = tf.einsum('bij,bjl->bil', self.unitary, 449 | input_state, name="Einsum_left") 450 | rightm = tf.einsum('bil,blj->bij', leftm, 451 | tf.linalg.adjoint(self.unitary), name="Einsum_right") 452 | 453 | return rightm 454 | 455 | def map_clements(self, params): 456 | """ 457 | gives a map_fn compatible version of tf_clements_stitch 458 | """ 459 | 460 | # segregate theta and phi parameters of the MZI 461 | theta = params[:self.element_num] 462 | phi = params[self.element_num:2*self.element_num] 463 | 464 | 465 | # compute unitary by hijacking clements decomposition code 466 | return tf_clements_stitch(beam_spec=self.bms_spec, 467 | theta=theta, phi=phi, 468 | diag=tf.constant([0.0]*self.modes, dtype=tf.float32), 469 | rev=tf.constant(False)) 470 | 471 | def compute_output_shape(self, input_shape): 472 | assert isinstance(input_shape, list) 473 | shape_a, shape_b = input_shape 474 | return [(shape_a[0], self.output_dim), shape_b[:-1]] 475 | 476 | def get_config(self): 477 | base_config = super(UParamLayer, self).get_config() 478 | base_config['output_dim'] = self.output_dim 479 | return base_config 480 | 481 | @classmethod 482 | def from_config(cls, config): 483 | return cls(**config) 484 | 485 | @property 486 | def output_size(self): 487 | return [self.output_dim, self.output_dim] 488 | 489 | 490 | class IsometricLayer(Layer): 491 | """ 492 | Advanced layer that acts as a kind of partial trace operation, decreasing the effective dimensionality 493 | of the output state in a non-trivial way. Force maps all photons detected in the ancilla modes into mode 1, 494 | so any variation in this scratch space is disregarded by the optimiser while maintaining the target modes and 495 | keeping the state space dimension constant. 496 | 497 | """ 498 | 499 | def __init__(self, modes, photons, idesc, force=False, **kwargs): 500 | # layer identifier 501 | self.id = "isometric" 502 | # number of modes 503 | self.modes = modes 504 | # number of variables on SU(modes) 505 | self.vars = (self.modes**2 - self.modes)//2 506 | # number of photons 507 | self.photons = photons 508 | # projection operator description 509 | self.idesc = idesc 510 | # emergent dimension 511 | self.input_dim = self.output_dim = comb( 512 | self.modes+self.photons-1, self.photons, exact=True) 513 | # catch extreme cases 514 | if self.input_dim > 100 and not force: 515 | raise ValueError( 516 | "System dimension is large ({}), decrease system dimension or set force flag to True".format(self.input_dim)) 517 | 518 | super(IsometricLayer, self).__init__(**kwargs) 519 | 520 | 521 | def build(self, input_shape): 522 | """ 523 | Construct projection operator for layer - non-trainable. 524 | Creates the variables of the layer (optional, for subclass implementers). 525 | 526 | This is a method that implements of subclasses of `Layer` or `Model` 527 | can override if they need a state-creation step in-between 528 | layer instantiation and layer call. 529 | 530 | This is typically used to create the weights of `Layer` subclasses. 531 | 532 | Arguments: 533 | input_shape: Instance of `TensorShape`, or list of instances of 534 | `TensorShape` if the layer expects a list of inputs 535 | (one instance per input). 536 | 537 | """ 538 | 539 | # returns an m x dim x dim array with m being the number of projectors to apply 540 | self.iso = iso_gen(self.idesc, convert=True) 541 | 542 | # call build method of super class 543 | super(IsometricLayer, self).build(input_shape) 544 | 545 | 546 | def call(self, inputs): 547 | """This is where the layer's logic lives. 548 | Arguments: 549 | inputs: Input tensor, or list/tuple of input tensors. 550 | **kwargs: Additional keyword arguments. 551 | 552 | Returns: 553 | A tensor or list/tuple of tensors. 554 | """ 555 | # compute effect of measurement projector on modes specified by photonic projectors 556 | left = tf.einsum('ijk,bkl->ibjl', self.iso, inputs, name='Projection_Left') 557 | # can skip adjoint calculation since projectors are all real diagonals (does that seem right?) 558 | right = tf.einsum('ibjl,ilk->ibjk', left, tf.linalg.adjoint(self.iso)) 559 | # collapse projector outcome sum into single, taking of batch broadcasting rules 560 | out = tf.reduce_sum(right, axis=[0], name='Projector_sum') 561 | 562 | return out 563 | 564 | 565 | def compute_output_shape(self, input_shape): 566 | shape = tf.TensorShape(input_shape).as_list() 567 | shape[-1] = self.output_dim 568 | return tf.TensorShape(shape) 569 | 570 | def get_config(self): 571 | base_config = super(IsometricLayer, self).get_config() 572 | base_config['output_dim'] = self.output_dim 573 | return base_config 574 | 575 | @classmethod 576 | def from_config(cls, config): 577 | return cls(**config) 578 | 579 | @property 580 | def output_size(self): 581 | return [self.output_dim, self.output_dim] 582 | 583 | 584 | class ProjectionLayer(Layer): 585 | """ 586 | Advanced layer that acts as a photonic detector on specified nodes. Allows for the possibility 587 | of returning to a pure state from a mixed one, a counter to the nonlinear layer. 588 | """ 589 | 590 | def __init__(self, modes, photons, pdesc, force=False, **kwargs): 591 | # layer identifier 592 | self.id = "projection" 593 | # number of modes 594 | self.modes = modes 595 | # number of variables on SU(modes) 596 | self.vars = (self.modes**2 - self.modes)//2 597 | # number of photons 598 | self.photons = photons 599 | # projection operator description 600 | self.pdesc = pdesc 601 | # emergent dimension 602 | self.input_dim = self.output_dim = comb( 603 | self.modes+self.photons-1, self.photons, exact=True) 604 | # catch extreme cases 605 | if self.input_dim > 100 and not force: 606 | raise ValueError( 607 | "System dimension is large ({}), decrease system dimension or set force flag to True".format(self.input_dim)) 608 | 609 | super(ProjectionLayer, self).__init__(**kwargs) 610 | 611 | 612 | def build(self, input_shape): 613 | """ 614 | Construct projection operator for layer - non-trainable. 615 | Creates the variables of the layer (optional, for subclass implementers). 616 | 617 | This is a method that implements of subclasses of `Layer` or `Model` 618 | can override if they need a state-creation step in-between 619 | layer instantiation and layer call. 620 | 621 | This is typically used to create the weights of `Layer` subclasses. 622 | 623 | Arguments: 624 | input_shape: Instance of `TensorShape`, or list of instances of 625 | `TensorShape` if the layer expects a list of inputs 626 | (one instance per input). 627 | 628 | """ 629 | 630 | # returns an m x dim x dim array with m being the number of projectors to apply 631 | self.proj = povm_gen(self.pdesc, convert=True) 632 | 633 | 634 | def call(self, inputs): 635 | """This is where the layer's logic lives. 636 | Arguments: 637 | inputs: Input tensor, or list/tuple of input tensors. 638 | **kwargs: Additional keyword arguments. 639 | 640 | Returns: 641 | A tensor or list/tuple of tensors. 642 | """ 643 | 644 | # compute effect of measurement projector on modes specified by photonic projectors 645 | left = tf.einsum('ijk,bkl->ibjl', self.proj, inputs, name='Projection_Left') 646 | # can skip adjoint calculation since projectors are all real diagonals (does that seem right?) 647 | right = tf.einsum('ibjl,ilk->ibjk', left, self.proj) 648 | # collapse projector outcome sum into single, taking of batch broadcasting rules 649 | out = tf.reduce_sum(right, axis=[0], name='Projector_sum') 650 | 651 | return out 652 | 653 | 654 | def compute_output_shape(self, input_shape): 655 | shape = tf.TensorShape(input_shape).as_list() 656 | shape[-1] = self.output_dim 657 | return tf.TensorShape(shape) 658 | 659 | def get_config(self): 660 | base_config = super(ProjectionLayer, self).get_config() 661 | base_config['output_dim'] = self.output_dim 662 | base_config['modes'] = self.modes 663 | base_config['vars'] = self.vars 664 | base_config['photons'] = self.photons 665 | base_config['pdesc'] = self.pdesc 666 | return base_config 667 | 668 | @classmethod 669 | def from_config(cls, config): 670 | return cls(**config) 671 | 672 | @property 673 | def output_size(self): 674 | return [self.output_dim, self.output_dim] 675 | 676 | 677 | class NoiseLayer(Layer): 678 | """ 679 | Simple Layer that applies a variety of noisy operations on a quantum optical network. 680 | """ 681 | 682 | def __init__(self, dim, noisedesc, force=False, **kwargs): 683 | # layer identifier 684 | self.id = "noise" 685 | # dimension of operator 686 | self.dim = dim 687 | # number of variables on SU(modes) 688 | self.vars = (self.dim**2 - self.dim)//2 689 | # non-linear operator description 690 | self.noisedesc = noisedesc 691 | 692 | # add in default values 693 | if "systematic" not in self.noisedesc: 694 | self.noisedesc["systematic"] = False 695 | 696 | if "pad" not in self.noisedesc: 697 | self.noisedesc["pad"] = 0 698 | 699 | # catch extreme cases 700 | if self.dim > 100 and not force: 701 | raise ValueError( 702 | "System dimension is large ({}), decrease system dimension or set force flag to True".format(self.dim)) 703 | 704 | # follow layer naming conventions 705 | self.input_dim = self.output_dim = self.dim 706 | 707 | # keep local copy of beam splitter decomposition table 708 | self.bms_spec = clements_phase_end(np.eye(self.dim))[0] 709 | 710 | super(NoiseLayer, self).__init__(**kwargs) 711 | 712 | def build(self, input_shape): 713 | """ 714 | Construct non-linear unitary to apply. 715 | Creates the variables of the layer (optional, for subclass implementers). 716 | 717 | This is a method that implements of subclasses of `Layer` or `Model` 718 | can override if they need a state-creation step in-between 719 | layer instantiation and layer call. 720 | 721 | This is typically used to create the weights of `Layer` subclasses. 722 | 723 | Arguments: 724 | input_shape: Instance of `TensorShape`, or list of instances of 725 | `TensorShape` if the layer expects a list of inputs 726 | (one instance per input). 727 | 728 | """ 729 | 730 | 731 | # define a systematic error unitary 732 | if self.noisedesc["systematic"]: 733 | # set random seed if supplied 734 | if "seed" in self.noisedesc: 735 | np.random.seed(self.noisedesc['seed']) 736 | 737 | # generate a random unitary and take a fractional power of it 738 | Unoise = scipy.linalg.fractional_matrix_power(randU(self.dim), self.noisedesc['s_noise']) 739 | 740 | # convert to tensorflow compatible object 741 | self.systematic = tf.convert_to_tensor(Unoise, dtype=tf.complex64) 742 | 743 | 744 | # call build method of super class 745 | super(NoiseLayer, self).build(input_shape) 746 | 747 | def call(self, inputs): 748 | """This is where the layer's logic lives. 749 | Arguments: 750 | inputs: Input tensor, or list/tuple of input tensors. 751 | **kwargs: Additional keyword arguments. 752 | 753 | Returns: 754 | A tensor or list/tuple of tensors. 755 | """ 756 | 757 | md = self.noisedesc["w_noise"] 758 | 759 | # superclass method for variable construction, get_variable has trouble with model awareness 760 | self.diag = backend.random_uniform(shape=[self.dim], dtype=tf.float32, minval=-np.pi*md, maxval=np.pi*md) 761 | 762 | self.theta = backend.random_uniform(shape=[self.vars], dtype=tf.float32, minval=0, maxval=np.pi*md/2) 763 | 764 | self.phi = backend.random_uniform(shape=[self.vars], dtype=tf.float32, minval=0, maxval=2*np.pi*md) 765 | 766 | # construct noisy operation on required number of modes 767 | self.unitary = tf_clements_stitch(self.bms_spec, self.theta, self.phi, self.diag) 768 | 769 | # apply systematic error 770 | if self.noisedesc["systematic"]: 771 | self.unitary = tf.matmul(self.unitary, self.systematic) 772 | 773 | # pad noise unitary if required 774 | if self.noisedesc["pad"] > 0: 775 | # pad operator dimension as many times as requested 776 | pad_op = [tf.constant([[1.0]], dtype=tf.complex64)]*self.noisedesc["pad"] 777 | pad_op = [self.unitary] + pad_op 778 | 779 | # perform block diagonal operator 780 | linop_blocks = [tf.linalg.LinearOperatorFullMatrix(block) for block in pad_op] 781 | # but y tho 782 | self.unitary = tf.convert_to_tensor(tf.linalg.LinearOperatorBlockDiag(linop_blocks).to_dense()) 783 | 784 | # perform matrix calculation using multiple einsums on input/output spaces 785 | leftm = tf.einsum('ij,bjl->bil', self.unitary, 786 | inputs, name="Einsum_left") 787 | output = tf.einsum('bil,lj->bij', leftm, 788 | tf.linalg.adjoint(self.unitary), name="Einsum_right") 789 | 790 | # # return probabalistic output 791 | # out = tf.math.scalar_mul(1-self.u_prob, inputs) + \ 792 | # tf.math.scalar_mul(self.u_prob, rightm) 793 | 794 | return output 795 | 796 | def compute_output_shape(self, input_shape): 797 | shape = tf.TensorShape(input_shape).as_list() 798 | shape[-1] = self.output_dim 799 | return tf.TensorShape(shape) 800 | 801 | def get_config(self): 802 | base_config = super(NoiseLayer, self).get_config() 803 | base_config['output_dim'] = self.output_dim 804 | return base_config 805 | 806 | @classmethod 807 | def from_config(cls, config): 808 | return cls(**config) 809 | 810 | @property 811 | def output_size(self): 812 | return [self.output_dim, self.output_dim] 813 | 814 | 815 | 816 | class NonLinearLayer(Layer): 817 | """ 818 | Simple Layer that implements a variety of nonlinear behaviours on a quantum optical network. 819 | """ 820 | 821 | def __init__(self, modes, photons, nldesc, force=False, **kwargs): 822 | # layer identifier 823 | self.id = "nonlinear" 824 | # number of modes 825 | self.modes = modes 826 | # number of variables on SU(modes) 827 | self.vars = (self.modes**2 - self.modes)//2 828 | # number of photons 829 | self.photons = photons 830 | # non-linear operator description 831 | self.nldesc = nldesc 832 | 833 | # emergent dimension 834 | self.input_dim = self.output_dim = comb( 835 | self.modes+self.photons-1, self.photons, exact=True) 836 | # catch extreme cases 837 | if self.input_dim > 100 and not force: 838 | raise ValueError( 839 | "System dimension is large ({}), decrease system dimension or set force flag to True".format(self.input_dim)) 840 | 841 | super(NonLinearLayer, self).__init__(**kwargs) 842 | 843 | def build(self, input_shape): 844 | """ 845 | Construct non-linear unitary to apply. 846 | Creates the variables of the layer (optional, for subclass implementers). 847 | 848 | This is a method that implements of subclasses of `Layer` or `Model` 849 | can override if they need a state-creation step in-between 850 | layer instantiation and layer call. 851 | 852 | Arguments: 853 | input_shape: Instance of `TensorShape`, or list of instances of 854 | `TensorShape` if the layer expects a list of inputs 855 | (one instance per input). 856 | 857 | """ 858 | # construct non-linear operator from dictionary description 859 | self.nltype, self.u_prob, self.unitary = nl_gen(self.nldesc, convert=True) 860 | 861 | # construct multiphoton unitary using either CPU intensive (slow) or memory hungry method 862 | if self.photons > 1 and np.shape(self.unitary)[0] == self.modes: 863 | # use symmetric map to compute multi photon unitary 864 | S = tf.constant(symmetric_map( 865 | self.modes, self.photons), dtype=tf.complex64) 866 | # map to product state then use symmetric isometry to reduce to isomorphic subspace 867 | self.unitary = tf.matmul(S, tf.matmul(tf_multikron( 868 | self.unitary, self.photons), tf.linalg.adjoint(S))) 869 | 870 | # call build method of super class 871 | super(NonLinearLayer, self).build(input_shape) 872 | 873 | def call(self, inputs): 874 | """This is where the layer's logic lives. 875 | Arguments: 876 | inputs: Input tensor, or list/tuple of input tensors. 877 | **kwargs: Additional keyword arguments. 878 | 879 | Returns: 880 | A tensor or list/tuple of tensors. 881 | """ 882 | 883 | # perform matrix calculation using multiple einsums 884 | leftm = tf.einsum('ij,bjl->bil', self.unitary, 885 | inputs, name="Einsum_left") 886 | rightm = tf.einsum('bil,lj->bij', leftm, 887 | tf.linalg.adjoint(self.unitary), name="Einsum_right") 888 | 889 | # return probabalistic output 890 | out = tf.math.scalar_mul(1-self.u_prob, inputs) + \ 891 | tf.math.scalar_mul(self.u_prob, rightm) 892 | 893 | return out 894 | 895 | def compute_output_shape(self, input_shape): 896 | shape = tf.TensorShape(input_shape).as_list() 897 | shape[-1] = self.output_dim 898 | return tf.TensorShape(shape) 899 | 900 | def get_config(self): 901 | base_config = super(NonLinearLayer, self).get_config() 902 | base_config['output_dim'] = self.output_dim 903 | return base_config 904 | 905 | @classmethod 906 | def from_config(cls, config): 907 | return cls(**config) 908 | 909 | @property 910 | def output_size(self): 911 | return [self.output_dim, self.output_dim] 912 | 913 | 914 | class InvertLayer(Layer): 915 | """ 916 | Advanced layer that encapsulates a fully connected dense layer while retaining invertibility. 917 | This layer is composed of bijective functions and thus is itself bijective! 918 | """ 919 | 920 | def __init__(self, dim, pad=0, init='glorot_uniform', permutation=True, force=False, **kwargs): 921 | # layer identifier 922 | self.id = "affine" 923 | # number of variables on SU(modes) 924 | self.var_num = dim 925 | # pad dimensions 926 | self.pad = pad 927 | # kernel initialiser 928 | self.kernel_init = init 929 | # whether to apply a fixed permutation matrix to input 930 | self.perm_flag = permutation 931 | # input dimension and output dimension match 932 | self.input_dim = self.output_dim = self.var_num + self.pad 933 | # split dimension 934 | self.split_dim = self.input_dim//2 935 | # layer direction flag 936 | self.invert = False 937 | # inheritance superclass 938 | super(InvertLayer, self).__init__(**kwargs) 939 | 940 | 941 | def build(self, input_shape): 942 | """ 943 | Construct projection operator for layer - non-trainable. 944 | Creates the variables of the layer (optional, for subclass implementers). 945 | 946 | This is a method that implements of subclasses of `Layer` or `Model` 947 | can override if they need a state-creation step in-between 948 | layer instantiation and layer call.0 949 | 950 | This is typically used to create the weights of `Layer` subclasses. 951 | 952 | Arguments: 953 | input_shape: Instance of `TensorShape`, or list of instances of 954 | `TensorShape` if the layer expects a list of inputs 955 | (one instance per input). 956 | 957 | """ 958 | 959 | # build a randomly generated fixed permutation if requested 960 | if self.perm_flag: 961 | pass 962 | #self.permutation = tf.convert_to_tensor() 963 | 964 | # build the internal models 965 | 966 | # call build method of super class 967 | super(InvertLayer, self).build(input_shape) 968 | 969 | def layer_apply(self, input_vec, kernel): 970 | """ 971 | Internal method for applying a layer to an input vector 972 | """ 973 | return tf.einsum('ij,bj->bi', kernel, input_vec, name="einsum_dense") 974 | 975 | 976 | 977 | def call(self, inputs): 978 | """This is where the layer's logic lives. 979 | Arguments: 980 | inputs: Input tensor, or list/tuple of input tensors. 981 | **kwargs: Additional keyword arguments. 982 | 983 | Returns: 984 | A tensor or list/tuple of tensors. 985 | """ 986 | 987 | # check if model is set to be inverted 988 | if self.invert: 989 | pass 990 | else: 991 | # split input into two evenly divided vectors 992 | u1, u2 = tf.split(inputs, num_or_size_splits=2, axis=1, name='split') 993 | 994 | # compute output vectors using bijective map 995 | s2 = self.layer_apply(u2, self.skernel2) 996 | t2 = self.layer_apply(u2, self.tkernel2) 997 | v1 = tf.math.multiply(u1, tf.math.exp(s2)) + t2 998 | 999 | s1 = self.layer_apply(v1, self.skernel1) 1000 | t1 = self.layer_apply(v1, self.tkernel1) 1001 | v2 = tf.math.multiply(u2, tf.math.exp(s1)) + t1 1002 | 1003 | # # compute effect of measurement projector on modes specified by photonic projectors 1004 | # left = tf.einsum('ijk,bkl->ibjl', self.proj, inputs, name='Projection_Left') 1005 | # # can skip adjoint calculation since projectors are all real diagonals (does that seem right?) 1006 | # right = tf.einsum('ibjl,ilk->ibjk', left, self.proj) 1007 | # # collapse projector outcome sum into single, taking of batch broadcasting rules 1008 | # out = tf.reduce_sum(right, axis=[0], name='Projector_sum') 1009 | 1010 | # concatenate output vectors and return 1011 | return tf.concat([v1, v2], axis=-1) 1012 | #return self.layer_apply(inputs, self.skernel) 1013 | 1014 | 1015 | def compute_output_shape(self, input_shape): 1016 | shape = tf.TensorShape(input_shape).as_list() 1017 | shape[-1] = self.output_dim 1018 | return tf.TensorShape(shape) 1019 | 1020 | def get_config(self): 1021 | base_config = super(InvertLayer, self).get_config() 1022 | base_config['output_dim'] = self.output_dim 1023 | base_config['vars'] = self.vars 1024 | return base_config 1025 | 1026 | @classmethod 1027 | def from_config(cls, config): 1028 | return cls(**config) 1029 | 1030 | @property 1031 | def output_size(self): 1032 | return [self.output_dim, self.output_dim] 1033 | 1034 | class RevDense(Layer): 1035 | """Just your regular densely-connected NN layer. 1036 | 1037 | `Dense` implements the operation: 1038 | `output = activation(dot(input, kernel) + bias)` 1039 | where `activation` is the element-wise activation function 1040 | passed as the `activation` argument, `kernel` is a weights matrix 1041 | created by the layer, and `bias` is a bias vector created by the layer 1042 | (only applicable if `use_bias` is `True`). 1043 | 1044 | Note: if the input to the layer has a rank greater than 2, then 1045 | it is flattened prior to the initial dot product with `kernel`. 1046 | 1047 | # Example 1048 | 1049 | ```python 1050 | # as first layer in a sequential model: 1051 | model = Sequential() 1052 | model.add(Dense(32, input_shape=(16,))) 1053 | # now the model will take as input arrays of shape (*, 16) 1054 | # and output arrays of shape (*, 32) 1055 | 1056 | # after the first layer, you don't need to specify 1057 | # the size of the input anymore: 1058 | model.add(Dense(32)) 1059 | ``` 1060 | 1061 | # Arguments 1062 | units: Positive integer, dimensionality of the output space. 1063 | activation: Activation function to use 1064 | (see [activations](../activations.md)). 1065 | If you don't specify anything, no activation is applied 1066 | (ie. "linear" activation: `a(x) = x`). 1067 | use_bias: Boolean, whether the layer uses a bias vector. 1068 | kernel_initializer: Initializer for the `kernel` weights matrix 1069 | (see [initializers](../initializers.md)). 1070 | bias_initializer: Initializer for the bias vector 1071 | (see [initializers](../initializers.md)). 1072 | kernel_regularizer: Regularizer function applied to 1073 | the `kernel` weights matrix 1074 | (see [regularizer](../regularizers.md)). 1075 | bias_regularizer: Regularizer function applied to the bias vector 1076 | (see [regularizer](../regularizers.md)). 1077 | activity_regularizer: Regularizer function applied to 1078 | the output of the layer (its "activation"). 1079 | (see [regularizer](../regularizers.md)). 1080 | kernel_constraint: Constraint function applied to 1081 | the `kernel` weights matrix 1082 | (see [constraints](../constraints.md)). 1083 | bias_constraint: Constraint function applied to the bias vector 1084 | (see [constraints](../constraints.md)). 1085 | 1086 | # Input shape 1087 | nD tensor with shape: `(batch_size, ..., input_dim)`. 1088 | The most common situation would be 1089 | a 2D input with shape `(batch_size, input_dim)`. 1090 | 1091 | # Output shape 1092 | nD tensor with shape: `(batch_size, ..., units)`. 1093 | For instance, for a 2D input with shape `(batch_size, input_dim)`, 1094 | the output would have shape `(batch_size, units)`. 1095 | """ 1096 | 1097 | def __init__(self, units, 1098 | activation=None, 1099 | use_bias=True, 1100 | kernel_initializer='glorot_uniform', 1101 | bias_initializer='zeros', 1102 | **kwargs): 1103 | if 'input_shape' not in kwargs and 'input_dim' in kwargs: 1104 | kwargs['input_shape'] = (kwargs.pop('input_dim'),) 1105 | super(RevDense, self).__init__(**kwargs) 1106 | self.units = units 1107 | self.use_bias = use_bias 1108 | self.kernel_initializer = kernel_initializer 1109 | self.bias_initializer = bias_initializer 1110 | self.supports_masking = True 1111 | self.invert = False 1112 | # call init method of super class 1113 | super(RevDense, self).__init__(**kwargs) 1114 | 1115 | def build(self, input_shape): 1116 | assert len(input_shape) >= 2 1117 | input_dim = input_shape[-1] 1118 | 1119 | self.kernel = self.add_weight(shape=(input_dim, self.units), 1120 | initializer=self.kernel_initializer, 1121 | name='kernel') 1122 | if self.use_bias: 1123 | self.bias = self.add_weight(shape=(self.units,), 1124 | initializer=self.bias_initializer, 1125 | name='bias') 1126 | else: 1127 | self.bias = None 1128 | self.built = True 1129 | 1130 | super(RevDense, self).build(input_shape) 1131 | 1132 | def call(self, inputs): 1133 | 1134 | if self.invert: 1135 | if self.use_bias: 1136 | output = inputs + self.bias 1137 | output = tf.einsum('ij,bj->bi', self.kernel, output, name="einsum_dense") 1138 | else: 1139 | output = tf.einsum('ij,bj->bi', self.kernel, inputs, name="einsum_dense") 1140 | else: 1141 | output = tf.einsum('ij,bj->bi', self.kernel, inputs, name="einsum_dense") 1142 | if self.use_bias: 1143 | output = output + self.bias 1144 | 1145 | return output 1146 | 1147 | def compute_output_shape(self, input_shape): 1148 | assert input_shape and len(input_shape) >= 2 1149 | assert input_shape[-1] 1150 | output_shape = list(input_shape) 1151 | output_shape[-1] = self.units 1152 | return tuple(output_shape) 1153 | 1154 | def invert_layer(self): 1155 | """ 1156 | Invert the network 1157 | """ 1158 | 1159 | # compute the inverse then reassign 1160 | self.kernel = tf.linalg.inv(self.kernel) 1161 | 1162 | self.bias = tf.multiply(-1.0, self.bias) 1163 | 1164 | # flip invert flag 1165 | self.invert = not self.invert 1166 | 1167 | def get_config(self): 1168 | config = { 1169 | 'units': self.units, 1170 | 'activation': activations.serialize(self.activation), 1171 | 'use_bias': self.use_bias, 1172 | 'kernel_initializer': initializers.serialize(self.kernel_initializer), 1173 | 'bias_initializer': initializers.serialize(self.bias_initializer), 1174 | } 1175 | base_config = super(RevDense, self).get_config() 1176 | return dict(list(base_config.items()) + list(config.items())) --------------------------------------------------------------------------------