Square ASI robustness to dilution defects
Contents
Square ASI robustness to dilution defects#
In this example we generate geometries of dilute square ASI and relax them by a rotating, global field. The results analyzed in terms of vertex type population and their robustness to increasing dilution fraction.
We first create the dilute ensembles from a square ASI. The ensembles are relaxed with a rotating field protocol in flatspin. Finally, we analyze the results with respect to vertex type populations.
Creating dilute ensembles#
First, we generate the example system: a complete square ASI. We will then iteratively remove magnets and save the resulting systems.
from flatspin.model import SquareSpinIceClosed, CustomSpinIce
from flatspin.plotting import plot_vector_image
size = (50, 50)
sq_asi = SquareSpinIceClosed(size = size, lattice_spacing = 1)
plt.figure(figsize=(10,6))
plt.title(f'Full original ensemble, size = {size}')
sq_asi.plot();
Next, we define the dilution fractions and the corresponding number of magnets to be removed at each dilution fraction.
asi_size_n = sq_asi.spin_count
dilutions = np.array([0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4])
num_removed = [round(d*asi_size_n) for d in dilutions]
print(f"For {asi_size_n} total magnets")
df = pd.DataFrame({'dilution fraction': dilutions, 'magnets removed': num_removed })
display(df)
For 5100 total magnets
dilution fraction | magnets removed | |
---|---|---|
0 | 0.001 | 5 |
1 | 0.002 | 10 |
2 | 0.005 | 26 |
3 | 0.010 | 51 |
4 | 0.020 | 102 |
5 | 0.050 | 255 |
6 | 0.100 | 510 |
7 | 0.150 | 765 |
8 | 0.200 | 1020 |
9 | 0.300 | 1530 |
10 | 0.400 | 2040 |
We now define the dilute geometries, using several random seeds for the stochastic removal of magnets.
The geometries are saved as files where the positions and angles of each spin are stored, to be used by the CustomSpinIce
model class.
root_dir = 'dilution'
if not os.path.exists(root_dir):
os.makedirs(root_dir)
print(f"Creating directory {root_dir}")
square_geom = (sq_asi.pos, sq_asi.angle)
random_iterations = 10
for rand_seed in range(random_iterations):
# Order the ids of the magnets. The order in which they are removed
np.random.seed(rand_seed)
removed_id = np.random.choice(asi_size_n, size=max(num_removed), replace=False)
for i, n in enumerate(num_removed):
square_geom_removed = (np.delete(square_geom[0], removed_id[:n], axis = 0),
np.delete(square_geom[1], removed_id[:n], axis = 0))
# Save coordinates and angles
np.savetxt(f'{root_dir}/coords-dilution-{str(i).zfill(2)}-seed-{str(rand_seed).zfill(2)}.csv', (square_geom_removed[0]))
np.savetxt(f'{root_dir}/angles-dilution-{str(i).zfill(2)}-seed-{str(rand_seed).zfill(2)}.csv', (square_geom_removed[1]))
Creating directory dilution
We can instantiate a CustomSpinIce example model from one of these generated geometries:
dilute_example = CustomSpinIce(magnet_coords = f'{root_dir}/coords-dilution-06-seed-00.csv',
magnet_angles = f'{root_dir}/angles-dilution-06-seed-00.csv',
radians = True)
plt.figure(figsize=(10,6))
plt.title(f'Dilute ensemble, size = {size}, dilution = {1-dilute_example.spin_count/asi_size_n:.2f}')
dilute_example.plot();
Baseline field annealing#
We test a field annealing approach for the undiluted system here, before we generate the runs for all diluted systems. See the field annealing documentation for an example of how to set this up.
First, we define some physical parameters of the simulation and a model instance:
# Size of magnets
w, l, t = 220e-9, 80e-9, 20e-9 # [m]
volume = w*l*t # [m^3]
M_sat = 860e3 # [A/m]
M_i = M_sat * volume
a = 1e-9 # lattice_spacing unit [m]
lattice_spacing = 300 # [a]
mu_0 = 1.25663706e-6 # m kg /s^2 A^2
alpha = mu_0 * M_i / (4 * np.pi * a**3)
# Astroid parameters
sw_b = 0.38
sw_c = 1
sw_beta = 1.3
sw_gamma = 3.6
rand_seed = 0
disorder = 0.05
neighbor_distance = 3
test_size = (10,10)
model = SquareSpinIceClosed(size=test_size, lattice_spacing=lattice_spacing, alpha=alpha,
disorder=disorder, neighbor_distance=neighbor_distance,
sw_b=sw_b, sw_c=sw_c, sw_beta=sw_beta, sw_gamma=sw_gamma,
random_seed=rand_seed, use_opencl=1)
Next, we use the Rotate
encoder and find some fitting field values that will relax the system with a rotating field.
We can adjust the parameter values to see the effect.
The current parameters are the ones used for the experiment in the paper, which takes a while to run, even for the smaller test system.
from flatspin.encoder import Rotate
# We define the decreasing field strength
H_hi, H_low = 0.080, 0.072
input_values = np.arange(1,0,-0.001)
timesteps = 100 # (per input value)
# Define the external field values based on the Rotate encoder
encoder = Rotate(H = H_hi, H0 = H_low, timesteps=timesteps)
h_ext = encoder(input_values)
# Start in polarized state
model.polarize()
# Record spins and number of spin flips over time
spins = []
flips = []
for i, h in enumerate(h_ext):
model.set_h_ext(h)
s = model.relax()
# Record spin state at the end of each rotation
if (i+1) % timesteps == 0:
spins.append(model.spin.copy())
flips.append(s)
print(f"Completed {sum(flips)} flips")
Completed 113676 flips
H = norm(h_ext, axis=-1).round(10)
df = pd.DataFrame({'H': H, 'flips': flips})
df.groupby('H', sort=False).sum().plot(figsize=(10,6))
plt.gca().invert_xaxis()
plt.ylabel("Spin flips")
plt.xlabel("Field strength [T]");
For a too strong (saturating) field strength, all the magnets will be flipped by the field. The maximum number of flips in one rotation of the field, i.e., at one field strength, is twice the total number of magnets (1 flip + 1 flip back). The number of timesteps and the field strength parameters should be tuned so that we start out flipping almost all the magnets (i.e., a strong enough field to change the system state) and go until we don’t flip any magnets as we don’t need to simulate weaker fields than that which results in no activity.
Below, we animate over the states of the relaxation process (skipping quite a few frames).
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
fig, ax = plt.subplots()
skip_frames = 20
def animate_spin(i):
i = i*skip_frames
H = norm(h_ext[(i+1) * timesteps - 1])
ax.set_title(f"H={H:.3f} [T]")
model.set_spin(spins[i])
model.plot_vertex_mag(ax=ax, replace=True)
anim = FuncAnimation(fig, animate_spin, frames=len(spins[::skip_frames]), interval=200, blit=False)
plt.close() # Only show the animation
HTML(anim.to_jshtml())
Create the runs to generate the data.#
We can now generate the data for all the dilute systems and different random seeds. We generate a command that can schedule a whole sweep of jobs based on the generated dilute geometries.
# Generate flatspin-run-sweep command
print("\nGENERATED COMMAND: \n")
print(f"flatspin-run-sweep -r dist -m CustomSpinIce -e Rotate ", end='\n')
print(f"-p sw_b={sw_b} -p sw_c={sw_c} -p sw_beta={sw_beta} -p sw_gamma={sw_gamma} ", end='\n')
print(f"-p alpha={alpha:.2f} -p lattice_spacing={lattice_spacing} -p disorder={disorder} ", end='\n')
print(f"-p neighbor_distance={neighbor_distance} ", end='\n')
print(f"-p 'input=np.arange(1,0,-0.001)' ", end='\n')
print(f"-p timesteps={timesteps} -p H={H_hi} -p H0={H_low} ", end='\n')
print(f"-s 'num=range(10)' -s 'rs=range(10)' ", end='\n')
print(f"-s 'magnet_coords=[\"coords-dilution-\"+str(num).zfill(2)+\"-seed-\"+str(rs).zfill(2)+\".csv\"]' ", end='\n')
print(f"-s 'magnet_angles=[\"angles-dilution-\"+str(num).zfill(2)+\"-seed-\"+str(rs).zfill(2)+\".csv\"]' ", end='\n')
print("-o CHANGE_TO_OUTPUT_FOLDER")
GENERATED COMMAND:
flatspin-run-sweep -r dist -m CustomSpinIce -e Rotate
-p sw_b=0.38 -p sw_c=1 -p sw_beta=1.3 -p sw_gamma=3.6
-p alpha=30272.00 -p lattice_spacing=300 -p disorder=0.05
-p neighbor_distance=3
-p 'input=np.arange(1,0,-0.001)'
-p timesteps=100 -p H=0.08 -p H0=0.072
-s 'num=range(10)' -s 'rs=range(10)'
-s 'magnet_coords=["coords-dilution-"+str(num).zfill(2)+"-seed-"+str(rs).zfill(2)+".csv"]'
-s 'magnet_angles=["angles-dilution-"+str(num).zfill(2)+"-seed-"+str(rs).zfill(2)+".csv"]'
-o CHANGE_TO_OUTPUT_FOLDER
Analyze generated data#
The dataset can also be downloaded here
.
from flatspin.data import Dataset, read_table, read_geometry, vector_grid
from flatspin.grid import Grid
from flatspin.vertices import find_vertices, vertex_type
data_path = '/data/flatspin/dilution-example-run'
D = Dataset.read(data_path)
display(D.index)
num | rs | magnet_coords | magnet_angles | outdir | |
---|---|---|---|---|---|
0 | 0 | 0 | rem-coords-00-seed-00.csv | rem-angles-00-seed-00.csv | CustomSpinIce.000000.npz |
1 | 0 | 1 | rem-coords-00-seed-01.csv | rem-angles-00-seed-01.csv | CustomSpinIce.000001.npz |
2 | 0 | 2 | rem-coords-00-seed-02.csv | rem-angles-00-seed-02.csv | CustomSpinIce.000002.npz |
3 | 0 | 3 | rem-coords-00-seed-03.csv | rem-angles-00-seed-03.csv | CustomSpinIce.000003.npz |
4 | 0 | 4 | rem-coords-00-seed-04.csv | rem-angles-00-seed-04.csv | CustomSpinIce.000004.npz |
... | ... | ... | ... | ... | ... |
95 | 9 | 5 | rem-coords-09-seed-05.csv | rem-angles-09-seed-05.csv | CustomSpinIce.000095.npz |
96 | 9 | 6 | rem-coords-09-seed-06.csv | rem-angles-09-seed-06.csv | CustomSpinIce.000096.npz |
97 | 9 | 7 | rem-coords-09-seed-07.csv | rem-angles-09-seed-07.csv | CustomSpinIce.000097.npz |
98 | 9 | 8 | rem-coords-09-seed-08.csv | rem-angles-09-seed-08.csv | CustomSpinIce.000098.npz |
99 | 9 | 9 | rem-coords-09-seed-09.csv | rem-angles-09-seed-09.csv | CustomSpinIce.000099.npz |
100 rows × 5 columns
We now count all the vertices and their types for all the different runs. This might take some time.
from collections import defaultdict
from joblib import Parallel, delayed
def count_types(ds):
dilution_num = int(ds.index['num'])
rand_seed = int(ds.index['rs'])
spins = read_table(ds.tablefile('spin'), index_col='t')
# Choose the final spin state at t=-1
spins_final = spins.iloc[-1]
geom = read_geometry(ds.tablefile('geometry'))
pos, angles = geom
grid = Grid(pos)
# Identify vertices (only complete vertices)
_, _, vertices = find_vertices(grid, pos, angles, 3)
# Sort vertices by type
types = [vertex_type([spins_final[s] for s in v],
[pos[s] for s in v],
[angles[s] for s in v])
for v in vertices]
# Count the vertex types
vertex_types, counts = np.unique(types, return_counts=True)
typecount = dict(zip(vertex_types, counts))
return dilution_num, rand_seed, counts
queue = D[0::10]
types_by_dilution = Parallel(n_jobs=10, verbose=0)(delayed(count_types)(ds) for ds in queue)
df_types = pd.DataFrame(types_by_dilution, columns={'dilution': 'num', 'rand_seed': 'rs', 'type': 'type'})
label_types = ['Type I', 'Type II', 'Type III', 'Type IV']
df_types[label_types[:-1]] = pd.DataFrame(df_types.type.tolist(), index= df_types.index)
df_types_avg = df_types.groupby('dilution').mean()
sum_magnets = df_types_avg[label_types[:-1]].sum(axis=1)
df_types_avg[[l+'_frac' for l in label_types[:-1]]] = (df_types_avg[label_types[:-1]].T/sum_magnets).T
df_types_avg
/tmp/ipykernel_2055/43424970.py:38: FutureWarning: The default value of numeric_only in DataFrameGroupBy.mean is deprecated. In a future version, numeric_only will default to False. Either specify numeric_only or select only columns which should be valid for the function.
df_types_avg = df_types.groupby('dilution').mean()
rand_seed | Type I | Type II | Type III | Type I_frac | Type II_frac | Type III_frac | |
---|---|---|---|---|---|---|---|
dilution | |||||||
0 | 0.0 | 1799.0 | 532.0 | 62.0 | 0.751776 | 0.222315 | 0.025909 |
1 | 0.0 | 1769.0 | 561.0 | 53.0 | 0.742342 | 0.235418 | 0.022241 |
2 | 0.0 | 1773.0 | 511.0 | 73.0 | 0.752227 | 0.216801 | 0.030972 |
3 | 0.0 | 1713.0 | 529.0 | 66.0 | 0.742201 | 0.229203 | 0.028596 |
4 | 0.0 | 1689.0 | 455.0 | 75.0 | 0.761154 | 0.205047 | 0.033799 |
5 | 0.0 | 1467.0 | 430.0 | 65.0 | 0.747706 | 0.219164 | 0.033129 |
6 | 0.0 | 1162.0 | 373.0 | 37.0 | 0.739186 | 0.237277 | 0.023537 |
7 | 0.0 | 927.0 | 285.0 | 31.0 | 0.745776 | 0.229284 | 0.024940 |
8 | 0.0 | 695.0 | 258.0 | 25.0 | 0.710634 | 0.263804 | 0.025562 |
9 | 0.0 | 386.0 | 152.0 | 18.0 | 0.694245 | 0.273381 | 0.032374 |
Plotting the results#
First, let’s look at each dilution fraction for one selected random seed.
def read_data_vertices_grid(dataset, t=[0]):
''' Return the positions and magnetization of the dataset's vertices '''
grid_size = None
crop_width = ((1,1),(1,1))
win_shape, win_step = ((3,3), (2, 2))
df = read_table(dataset.tablefile('mag'), index_col='t')
UV = np.array(df)
UV = UV.reshape((UV.shape[0], -1, 2))
UV = UV[t]
pos, angle = read_geometry(dataset.tablefile('geometry'))
XY, UV = vector_grid(pos, UV, grid_size, crop_width, win_shape, win_step, normalize=False)
return XY, UV/2
def plot_vertices(XY, UVi, ax):
ax.axis('off')
plot_vector_image(XY, UVi, ax=ax, replace=True)
# Load final states (might take some time)
final_dilution_states = dict()
for num, ds in D[D.index['rs']==0].groupby('num'):
final_dilution_states[dilutions[num]] = read_data_vertices_grid(ds, t=-1)
# Plot final states
fig, axs = plt.subplots(2, 5, figsize=(10,4), dpi=100)
for ax, (dilution, vstate) in zip(axs.flat, final_dilution_states.items()):
ax.set_title(f'dilution = {dilution}')
XY, UVi = vstate
mag = np.amax(np.linalg.norm(UVi, axis=-1))
plot_vertices(XY, UVi/mag, ax)
fig.suptitle('Final timestep', fontsize='x-large');
Finally, we plot the vertex type as a function of dilution fraction.
df_types_avg['Type IV_frac'] = 0.0
plt.figure(figsize=(10,6))
for label, ts in zip(label_types, df_types_avg[[l+'_frac' for l in label_types]]):
plt.plot(dilutions[:-1], df_types_avg[ts], marker = 'x', label=label)
plt.xscale('log')
plt.ylabel('Average vertex fraction')
plt.xlabel('Dilution fraction')
plt.legend();