├── LICENSE ├── README.md ├── halbachFields.py └── homogeneityOptimisation.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tom O’Reilly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HalbachOptimisation 2 | An implementation of a genetic algorithm to optimise the homogeneity of a Halbach array by varying the ring diameters along the length of the Halbach cylinder 3 | -------------------------------------------------------------------------------- /halbachFields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Feb 13 18:26:51 2018 4 | 5 | @author: to_reilly 6 | """ 7 | import numpy as np 8 | import scipy.ndimage as nd 9 | 10 | mu = 1e-7 11 | 12 | def magnetization(bRem, dimensions, shape = 'cube'): 13 | if shape == 'cube': 14 | dip_mom = bRem * dimensions**3 / (4*np.pi*mu) 15 | return dip_mom 16 | 17 | def singleMagnet(position, dipoleMoment, simDimensions, resolution): 18 | 19 | #create mesh coordinates 20 | x = np.linspace(-simDimensions[0]/2 + position[0], simDimensions[0]/2 + position[0], int(simDimensions[0]*resolution+1), dtype=np.float32) 21 | y = np.linspace(-simDimensions[1]/2 + position[1], simDimensions[1]/2 + position[1], int(simDimensions[1]*resolution+1), dtype=np.float32) 22 | z = np.linspace(-simDimensions[2]/2 + position[2], simDimensions[2]/2 + position[2], int(simDimensions[2]*resolution+1), dtype=np.float32) 23 | x, y, z = np.meshgrid(x,y,z) 24 | 25 | vec_dot_dip = 3*(x*dipoleMoment[0] + y*dipoleMoment[1]) 26 | 27 | #calculate the distance of each mesh point to magnet, optimised for speed 28 | #for improved memory performance move in to b0 calculations 29 | vec_mag = np.square(x) + np.square(y) + np.square(z) 30 | vec_mag_3 = np.power(vec_mag,1.5) 31 | vec_mag_5 = np.power(vec_mag,2.5) 32 | del vec_mag 33 | 34 | B0 = np.zeros((int(simDimensions[0]*resolution)+1,int(simDimensions[1]*resolution)+1,int(simDimensions[2]*resolution)+1,3), dtype=np.float32) 35 | 36 | #calculate contributions of magnet to total field, dipole always points in xy plane 37 | #so second term is zero for the z component 38 | B0[:,:,:,0] += np.divide(np.multiply(x, vec_dot_dip),vec_mag_5) - np.divide(dipoleMoment[0],vec_mag_3) 39 | B0[:,:,:,1] += np.divide(np.multiply(y, vec_dot_dip),vec_mag_5) - np.divide(dipoleMoment[1],vec_mag_3) 40 | B0[:,:,:,2] += np.divide(np.multiply(z, vec_dot_dip),vec_mag_5) 41 | 42 | return B0 43 | 44 | def createHalbach(numMagnets = 24, rings = (-0.075,-0.025, 0.025, 0.075), radius = 0.145, magnetSize = 0.0254, kValue = 2, resolution = 1000, bRem = 1.3, simDimensions = (0.3, 0.3, 0.2)): 45 | 46 | #define vacuum permeability 47 | mu = 1e-7 48 | 49 | #positioning of the magnets in a circle 50 | angle_elements = np.linspace(0, 2*np.pi, numMagnets, endpoint=False) 51 | 52 | #Use the analytical expression for the z component of a cube magnet to estimate 53 | #dipole momentstrength for correct scaling. Dipole approximation only valid 54 | #far-ish away from magnet, comparison made at 1 meter distance. 55 | 56 | dip_mom = magnetization(bRem, magnetSize) 57 | 58 | #create array to store field data 59 | B0 = np.zeros((int(simDimensions[0]*resolution)+1,int(simDimensions[1]*resolution)+1,int(simDimensions[2]*resolution)+1,3), dtype=np.float32) 60 | 61 | #create halbach array 62 | for row in rings: 63 | for angle in angle_elements: 64 | position = (radius*np.cos(angle),radius*np.sin(angle), row) 65 | 66 | dip_vec = [dip_mom*np.cos(kValue*angle), dip_mom*np.sin(kValue*angle)] 67 | dip_vec = np.multiply(dip_vec,mu) 68 | 69 | #calculate contributions of magnet to total field, dipole always points in xy plane 70 | #so second term is zero for the z component 71 | B0 += singleMagnet(position, dip_vec, simDimensions, resolution) 72 | 73 | return B0 74 | -------------------------------------------------------------------------------- /homogeneityOptimisation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Sep 24 16:17:41 2018 4 | 5 | @author: to_reilly 6 | """ 7 | 8 | import numpy as np 9 | import halbachFields 10 | import random 11 | from deap import algorithms, base, tools,creator 12 | import multiprocessing 13 | import ctypes 14 | import time 15 | import matplotlib.pyplot as plt 16 | 17 | 18 | def fieldError(shimVector): 19 | field = np.zeros(np.size(sharedShimMagnetsFields,0)) 20 | for idx1 in range(0,np.size(shimVector)): 21 | field += sharedShimMagnetsFields[:,idx1,shimVector[idx1]] 22 | return (((np.max(field)-np.min(field))/np.mean(field))*1e6,) 23 | 24 | 25 | if __name__ == "__main__": 26 | 27 | innerRingRadii = np.array([148, 151, 154, 156, 159, 162, 165, 168, 171, 174, 177, 180, 183, 186, 189, 192, 195, 198, 201])*1e-3 28 | innerNumMagnets = np.array([50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69]) 29 | 30 | outerRingRadii = innerRingRadii + 21*1e-3 31 | outerNumMagnets = innerNumMagnets + 7 32 | 33 | resolution = 5 34 | numRings = 23 35 | ringSep = .022 36 | magnetLength = (numRings - 1) * ringSep 37 | ringPositions = np.linspace(-magnetLength/2, magnetLength/2, numRings) 38 | 39 | 40 | #population 41 | popSim = 10000 42 | maxGeneration = 100 43 | 44 | DSV = 200*1e-3 45 | 46 | ################################################################################################################### 47 | ########################################## ########################################## 48 | ########################################## Create spherical mask ########################################## 49 | ########################################## ########################################## 50 | ################################################################################################################### 51 | 52 | simDimensions = (DSV,DSV,DSV) 53 | 54 | coordinateAxis = np.linspace(-simDimensions[0]/2,simDimensions[0]/2,int(1e3*simDimensions[0]/resolution + 1)) 55 | coords = np.meshgrid(coordinateAxis, coordinateAxis, coordinateAxis) 56 | 57 | mask = np.zeros(np.shape(coords[0])) 58 | mask[np.square(coords[0]) + np.square(coords[1]) + np.square(coords[2]) <= (DSV/2)**2] = 1 59 | 60 | octantMask = np.copy(mask) 61 | octantMask[coords[0] < 0] = 0 62 | octantMask[coords[1] < 0] = 0 63 | octantMask[coords[2] < 0] = 0 64 | 65 | ringPositionsSymmetery = ringPositions[ringPositions >= 0] 66 | 67 | shimFields = np.zeros((int(np.sum(octantMask)), np.size(ringPositionsSymmetery), np.size(innerRingRadii))) 68 | 69 | for positionIdx, position in enumerate(ringPositionsSymmetery): 70 | for sizeIdx, ringSize in enumerate(innerRingRadii): 71 | if position == 0: 72 | rings = (0,) 73 | else: 74 | rings = (-position, position) 75 | fieldData = halbachFields.createHalbach(numMagnets = innerNumMagnets[sizeIdx], rings = rings, radius = innerRingRadii[sizeIdx], magnetSize = 0.012, resolution = 1e3/resolution, simDimensions = simDimensions) 76 | fieldData += halbachFields.createHalbach(numMagnets = outerNumMagnets[sizeIdx], rings = rings, radius = outerRingRadii[sizeIdx], magnetSize = 0.012, resolution = 1e3/resolution, simDimensions = simDimensions) 77 | shimFields[:,positionIdx, sizeIdx] = fieldData[octantMask == 1,0] 78 | 79 | 80 | sharedShimMagnetsFields_base = multiprocessing.Array(ctypes.c_double, np.size(shimFields)) 81 | sharedShimMagnetsFields = np.ctypeslib.as_array(sharedShimMagnetsFields_base.get_obj()) 82 | sharedShimMagnetsFields = sharedShimMagnetsFields.reshape(np.size(shimFields,0),np.size(shimFields,1),np.size(shimFields,2)) 83 | sharedShimMagnetsFields[...] = shimFields[...] 84 | 85 | random.seed() 86 | 87 | creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) 88 | creator.create("Individual", list, fitness=creator.FitnessMin) 89 | 90 | toolbox = base.Toolbox() 91 | 92 | toolbox.register("attr_bool", random.randint,0,np.size(sharedShimMagnetsFields,2)-1) 93 | 94 | toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_bool, np.size(sharedShimMagnetsFields, 1)) 95 | 96 | toolbox.register("population", tools.initRepeat, list, toolbox.individual) 97 | 98 | toolbox.register("evaluate", fieldError) 99 | 100 | toolbox.register("mate", tools.cxTwoPoint) 101 | 102 | toolbox.register("mutate", tools.mutFlipBit, indpb=0.05) 103 | 104 | toolbox.register("select", tools.selTournament, tournsize=3) 105 | 106 | pop = toolbox.population(n=popSim) 107 | CXPB, MUTPB = 0.55, 0.4 108 | print("Start of evolution") 109 | startTime = time.time() 110 | fitnesses = list(map(toolbox.evaluate, pop)) 111 | bestError = np.inf 112 | 113 | for ind, fit in zip(pop, fitnesses): 114 | ind.fitness.values = fit 115 | 116 | fits = [ind.fitness.values[0] for ind in pop] 117 | 118 | # Variable keeping track of the number of generations 119 | g = 0 120 | minTracker = np.zeros((maxGeneration)) 121 | startEvolution = time.time() 122 | # Begin the evolution 123 | while g < maxGeneration: 124 | startTime = time.time() 125 | # A new generation 126 | g = g + 1 127 | print("-- Generation %i --" % g) 128 | 129 | # Select the next generation individuals 130 | offspring = toolbox.select(pop, len(pop)) 131 | # Clone the selected individuals 132 | offspring = list(map(toolbox.clone, offspring)) 133 | 134 | # Apply crossover and mutation on the offspring 135 | for child1, child2 in zip(offspring[::2], offspring[1::2]): 136 | 137 | # cross two individuals with probability CXPB 138 | if random.random() < CXPB: 139 | toolbox.mate(child1, child2) 140 | 141 | # fitness values of the children 142 | # must be recalculated later 143 | del child1.fitness.values 144 | del child2.fitness.values 145 | 146 | for mutant in offspring: 147 | 148 | # mutate an individual with probability MUTPB 149 | if random.random() < MUTPB: 150 | toolbox.mutate(mutant) 151 | del mutant.fitness.values 152 | 153 | # Evaluate the individuals with an invalid fitness 154 | invalid_ind = [ind for ind in offspring if not ind.fitness.valid] 155 | fitnesses = map(toolbox.evaluate, invalid_ind) 156 | for ind, fit in zip(invalid_ind, fitnesses): 157 | ind.fitness.values = fit 158 | 159 | 160 | # The population is entirely replaced by the offspring 161 | pop[:] = offspring 162 | 163 | # Gather all the fitnesses in one list and print the stats 164 | fits = [ind.fitness.values[0] for ind in pop] 165 | 166 | if min(fits) < bestError: 167 | #best in a generation is not per se best ever due to mutations, this tracks best ever 168 | print("BEST VECTOR: " + str(tools.selBest(pop, 1)[0])) 169 | bestError = min(fits) 170 | actualBestVector = tools.selBest(pop, 1)[0] 171 | 172 | minTracker[g-1]= min(fits) 173 | print("Evaluation took " + str(time.time()-startTime) + " seconds") 174 | print("Minimum: %i ppm" % min(fits)) 175 | 176 | print("-- End of (successful) evolution --") 177 | best_ind = tools.selBest(pop, 1)[0] 178 | print("Best individual is %s, %s" % (best_ind, best_ind.fitness.values)) 179 | 180 | 181 | bestVector = np.array(actualBestVector) 182 | 183 | shimmedField = np.zeros(np.shape(mask)) 184 | for positionIdx, position in enumerate(ringPositionsSymmetery): 185 | if position == 0: 186 | rings = (0,) 187 | else: 188 | rings = (-position, position) 189 | shimmedField += halbachFields.createHalbach(numMagnets = innerNumMagnets[bestVector[positionIdx]], rings = rings, radius = innerRingRadii[bestVector[positionIdx]], magnetSize = 0.012, resolution = 1e3/resolution, simDimensions = simDimensions)[...,0] 190 | shimmedField += halbachFields.createHalbach(numMagnets = outerNumMagnets[bestVector[positionIdx]], rings = rings, radius = outerRingRadii[bestVector[positionIdx]], magnetSize = 0.012, resolution = 1e3/resolution, simDimensions = simDimensions)[...,0] 191 | 192 | mask[mask == 0] = np.nan 193 | 194 | maskedField = np.abs(np.multiply(shimmedField,mask)) 195 | 196 | print("Shimmed mean: %.2f mT"%(1e3*np.nanmean(maskedField))) 197 | print("Shimmed homogeneity: %.4f mT" %(1e3*(np.nanmax(maskedField)-np.nanmin(maskedField)))) 198 | print("Shimmed homogeneity: %i ppm" %(1e6*((np.nanmax(maskedField)-np.nanmin(maskedField))/np.nanmean(maskedField)))) 199 | 200 | plt.figure() 201 | plt.plot(coordinateAxis*1e3, maskedField[int(np.floor(np.size(maskedField,0)/2)),int(np.floor(np.size(maskedField,1)/2)),:]) 202 | plt.xlabel('X axis (mm)') 203 | plt.ylabel('Field strength (Tesla)') 204 | plt.legend() 205 | 206 | plt.figure() 207 | plt.semilogy(minTracker) 208 | plt.title("Min error Vs generations") 209 | plt.xlabel("Generation") 210 | plt.ylabel("Error") 211 | 212 | fig, ax = plt.subplots(1,3) 213 | ax[0].imshow(maskedField[:,:,int(np.floor(np.size(maskedField,2)/2))]) 214 | ax[1].imshow(maskedField[:,int(np.floor(np.size(maskedField,1)/2)),:]) 215 | ax[2].imshow(maskedField[int(np.floor(np.size(maskedField,0)/2)),:,:]) 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | --------------------------------------------------------------------------------