def optimize_basic(self, verbose=False): # optimizes a clements mesh by attempting to get close to target each layer """ BASIC IDEA: Go through each layer from left to right. At each step, try to get the power output of this layer equal to the eventual target output. Once the optimization gives up, move to the next layer. """ # loop through layers # bar = ProgressBar(max_value=self.M) for layer_index in range(self.M): if verbose: print('working on layer {} of {}'.format(layer_index, self.M)) # bar.update(layer_index) # get previous powers and layer values_prev = self.mesh.partial_values[layer_index] layer = self.mesh.layers[layer_index] # desired output powers = target outputs D = power_vec(self.output_target) new_layer = self.tune_layer(L=layer, input_values=values_prev, desired_power=D, verbose=verbose) # insert into the mesh and recompute / recouple self.mesh.layers[layer_index] = new_layer self.mesh.recompute_matrices() self.mesh.input_couple(self.input_values)
def get_layer_powers(self, layer_index): # returns the power right BEFORE layer index (0 = input) if not self.coupled: raise ValueError( "must run `Mesh.input_couple(input_values)` before getting layer powers" ) partial_values = self.partial_values[layer_index] return power_vec(partial_values)
def objfn(phis, *args): offset_map, input_values, desired_power = args L = construct_layer(offset_map, phis, N=input_values.size) matrix = L.M out_values = np.dot(matrix, input_values) out_power = power_vec(out_values) # return MSE with desired return MSE(out_power, desired_power)
def objfn(phis, *args): mzi, input_values, desired_power = args mzi.phi1 = phis[0] mzi.phi2 = phis[1] matrix = mzi.M out_values = np.dot(matrix, input_values) out_power = power_vec(out_values) # return MSE with desired return MSE(out_power, desired_power)
def check_power(self, mesh, output_target): # checks if output equals target P_out = power_vec(mesh.output_values) P_target = power_vec(output_target) error = norm(P_out - P_target) / self.N self.assertLess(error, EPSILON)
def optimize_spread(self, verbose=False): # optimizes a triangular mesh by spreading power when possible """ BASIC IDEA: The problem with up down is the power gets concentrated at the top Here, we try to spread the power evenly in the middle section. Only push power up if it is needed in the top out ports. Otherwise, keep power distributed. """ # target output powers P = power_vec(self.output_target) P0 = P[0] # input powers I = power_vec(self.mesh.partial_values[0]) # middle section powers M = np.zeros((self.N, 1)) # iterate from bottom to top for layer_index in range(self.M // 2): # get the layer, previous field values, and port index layer = self.mesh.layers[layer_index] values_prev = self.mesh.partial_values[layer_index] port_index = (self.N - 1) - layer_index # construct a 'desired' power array for the output of this layer (equal to previous powers to start) D = power_vec(values_prev) # sum the desired powers that are supplied by this port P_sum = np.sum(P[port_index - 1:]) # sum the existing middle powers that can also contribute M_sum = np.sum(D[port_index + 1:]) # the remaining power to be spread over the middle ports P_rem = 1 - P_sum # split this remaining power evenly between midle ports P_avg = (1 - P0 - M_sum) / (self.N - 1 - layer_index) # the output port is the minimum of the average power and the required power D[port_index] = min(P_avg, P_sum - M_sum) # the lower output port is just the sum of the remaining power D[port_index - 1] = 0 D[port_index - 1] = 1 - np.sum(D) # tune the layer MZI and move on new_layer = self.tune_layer(L=layer, input_values=values_prev, desired_power=D, verbose=verbose) self.mesh.layers[layer_index] = new_layer self.mesh.recompute_matrices() self.mesh.input_couple(self.input_values) # loop from top down for layer_index in range(self.M // 2, self.M): # get previous values, powers, and layer values_prev = self.mesh.partial_values[layer_index] powers_prev = power_vec(values_prev) layer = self.mesh.layers[layer_index] # computes the port index port_index = layer_index - self.M // 2 # desired powers. D = np.zeros((self.N, 1)) # we know the desired power of this port is just the target D[port_index] = P[port_index] # the sum of powers into this MZI P_in = np.sum(powers_prev[port_index:port_index + 2]) # the other port target power should just be the remaining power D[port_index + 1] = P_in - P[port_index] # tune the layer and move on new_layer = self.tune_layer(L=layer, input_values=values_prev, desired_power=D, verbose=verbose) self.mesh.layers[layer_index] = new_layer self.mesh.recompute_matrices() self.mesh.input_couple(self.input_values)
def optimize_up_down(self, verbose=False): # optimizes a triangular mesh by two step process """ BASIC IDEA: With the upward pass, we can push all of the power into the top MZI Then, on the downward pass, we can redistribute the power to the output ports as it is needed. This is simple and effective, but concentrates power, which isn't good for DLA. See 'spread' algorithm for an improvement """ # loop throgh MZI from bottom layers to top for layer_index in range(self.M // 2): # get the previous field values, the current layer, and the port index values_prev = self.mesh.partial_values[layer_index] layer = self.mesh.layers[layer_index] port_index = (self.N - 1) - layer_index # make desired output vector for this layer 'D' # all of the power from this MZI should go to the top port D = np.zeros((self.N, 1)) D[port_index - 1] = 1 # tune the layer new_layer = self.tune_layer(L=layer, input_values=values_prev, desired_power=D, verbose=verbose) # insert into the mesh and recompute / recouple self.mesh.layers[layer_index] = new_layer self.mesh.recompute_matrices() self.mesh.input_couple(self.input_values) # loop throgh MZI from top layers to bottom for layer_index in range(self.M // 2, self.M): # get the previous field values, the current layer, and the port index values_prev = self.mesh.partial_values[layer_index] layer = self.mesh.layers[layer_index] port_index = layer_index - self.M // 2 # output target powers P = power_vec(self.output_target) # make desired output vector for this layer 'D' D = np.zeros((self.N, 1)) # the desired output power for this port is the target output D[port_index] = P[port_index] # the desired output power for the next port is the remaining power D[port_index + 1] = 1 - np.sum(P[:port_index + 1]) # set this layer new_layer = self.tune_layer(L=layer, input_values=values_prev, desired_power=D, verbose=verbose) # insert into the mesh and recompute / recouple self.mesh.layers[layer_index] = new_layer self.mesh.recompute_matrices() self.mesh.input_couple(self.input_values)
def optimize_smart_sequential(self, verbose=False): # optimizes a clements mesh by attempting to get close to target each layer """ BASIC IDEA: Go through each layer from left to right. At each MZI in the layer, push as much power up or down depending on what is needed and supplied. """ # powers needed above and below each port P = power_vec(self.output_target) Ps_up = np.zeros((self.N, )) Ps_down = np.zeros((self.N, )) for port_index in range(self.N): Ps_up[port_index] = np.sum(P[:port_index + 1]) Ps_down[port_index] = np.sum(P[port_index + 1:]) # loop through layers # bar = ProgressBar(max_value=self.M) for layer_index in range(self.M): if verbose: print('working on layer {} of {}'.format(layer_index, self.M)) # bar.update(layer_index) # get previous powers and layer values_prev = self.mesh.partial_values[layer_index] powers_prev = power_vec(values_prev) Ins_up = np.zeros((self.N, )) Ins_down = np.zeros((self.N, )) for port_index in range(self.N): Ins_up[port_index] = np.sum(powers_prev[:port_index]) Ins_down[port_index] = np.sum(powers_prev[port_index + 2:]) layer = self.mesh.layers[layer_index] # desired output powers = target outputs D = np.zeros((self.N, )) if layer_index % 2 == 0: top_port_indeces = range(0, self.N - 1, 2) else: top_port_indeces = range(1, self.N - 1, 2) new_mzis = [] for top_port_index in top_port_indeces: I_top = powers_prev[top_port_index, 0] I_bot = powers_prev[top_port_index + 1, 0] P_in_MZI = I_top + I_bot P_up = Ps_up[top_port_index] P_down = Ps_down[top_port_index] I_up = Ins_up[top_port_index] I_down = Ins_down[top_port_index] P_needed_up = P_up - I_up P_needed_down = P_down - I_down if P_up > (I_up + P_in_MZI): # more power needed in top ports than can be supplied now # push all up D[top_port_index] = P_in_MZI D[top_port_index + 1] = 0 elif P_up > I_up: # power needed in top ports but not so more than suppliable by MZI # push needed up D[top_port_index] = P_up - I_up D[top_port_index + 1] = P_in_MZI - D[top_port_index] elif P_down > (I_down + P_in_MZI): # more power needed in bottom ports than can be supplied now # push all down D[top_port_index + 1] = P_in_MZI D[top_port_index] = 0 elif P_down > I_down: # power needed in bottom ports but not so more than suppliable by MZI # push needed down, rest up D[top_port_index + 1] = P_down - I_down D[top_port_index] = P_in_MZI - D[top_port_index + 1] old_mzi = layer.mzis[top_port_index] new_mzi = self.tune_mzi( mzi=old_mzi, input_values=values_prev[top_port_index:top_port_index + 2], desired_power=D[top_port_index:top_port_index + 2], verbose=verbose) layer.embed_MZI(new_mzi, top_port_index) # # insert into the mesh and recompute / recouple self.mesh.layers[layer_index] = layer self.mesh.recompute_matrices() self.mesh.input_couple(self.input_values)
# stores the convergences. convergences = np.zeros((N_max, N_max)) # for each mesh size for N in N_list: M = N_max # construct an NxN clements mesh. mesh = Mesh(N, mesh_type='clements', initialization='random', M=M) print('N = {} / {}:'.format(N, N_max)) print(mesh) # uniform output target output_target = np.ones((N, )) target_power = power_vec(normalize_vec(output_target)) # store MSE of each layer in each run in averaging mses = M * [0.] # for N_avg random inputs (to average over) for _ in range(N_avg): # make random inputs and couple them in input_values = npr.random((N, 1)) mesh.input_couple(input_values) # make a clements mesh and optimize using the 'smart' algorithm CO = ClementsOptimizer(mesh, input_values=input_values, output_target=output_target)