llgs-simulation 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llgs/LLGS_simulation.py +373 -0
- llgs/__init__.py +4 -0
- llgs/interactions.py +144 -0
- llgs/lattice.py +177 -0
- llgs/plotting.py +165 -0
- llgs/read_results.py +61 -0
- llgs/spin_velocity.py +99 -0
- llgs_simulation-0.1.0.dist-info/METADATA +196 -0
- llgs_simulation-0.1.0.dist-info/RECORD +12 -0
- llgs_simulation-0.1.0.dist-info/WHEEL +5 -0
- llgs_simulation-0.1.0.dist-info/licenses/LICENSE +21 -0
- llgs_simulation-0.1.0.dist-info/top_level.txt +1 -0
llgs/LLGS_simulation.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Literal, Optional, Sequence, Tuple, Union
|
|
4
|
+
|
|
5
|
+
from numba import njit
|
|
6
|
+
import h5py
|
|
7
|
+
from scipy import sparse
|
|
8
|
+
from tqdm import tqdm
|
|
9
|
+
|
|
10
|
+
from .lattice import Lattice_2D
|
|
11
|
+
from .spin_velocity import (
|
|
12
|
+
calculate_spin_velocities_csr_jit,
|
|
13
|
+
calculate_spin_velocities_jit,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
Matrix = Union[np.ndarray, sparse.spmatrix]
|
|
17
|
+
DMIField = Union[np.ndarray, Sequence[Matrix]]
|
|
18
|
+
|
|
19
|
+
@njit
|
|
20
|
+
def normalize(spins):
|
|
21
|
+
return spins/np.sqrt(np.sum(spins**2, axis=1)[:,None])
|
|
22
|
+
|
|
23
|
+
class LLGS_Simulation_2D:
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
lattice: Lattice_2D,
|
|
28
|
+
H_E: Optional[Matrix] = None,
|
|
29
|
+
H_DMI: Optional[DMIField] = None,
|
|
30
|
+
H_ext: Optional[Sequence[float]] = None,
|
|
31
|
+
H_FL: Optional[Sequence[float]] = None,
|
|
32
|
+
H_DL: Optional[Sequence[float]] = None,
|
|
33
|
+
H_perp: float = 0,
|
|
34
|
+
H_para: float = 0,
|
|
35
|
+
phi_a: float = 0,
|
|
36
|
+
alpha: float = 0,
|
|
37
|
+
method: str = "RK4",
|
|
38
|
+
io_foldername: Union[str, Path] = "lattice",
|
|
39
|
+
io_filename: Optional[str] = None,
|
|
40
|
+
io_compress: bool = True,
|
|
41
|
+
io_screen: bool = True,
|
|
42
|
+
matrix_type: Literal["sparse", "dense", "auto"] = "auto",
|
|
43
|
+
) -> None:
|
|
44
|
+
self.lattice = lattice
|
|
45
|
+
self.N = lattice.N
|
|
46
|
+
if H_E is None:
|
|
47
|
+
self.H_E = np.zeros((self.N, self.N))
|
|
48
|
+
elif sparse.issparse(H_E):
|
|
49
|
+
self.H_E = H_E
|
|
50
|
+
else:
|
|
51
|
+
self.H_E = np.asarray(H_E)
|
|
52
|
+
if self.H_E.shape != (self.N, self.N):
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"H_E must have shape ({self.N}, {self.N}), got {self.H_E.shape}"
|
|
55
|
+
)
|
|
56
|
+
self.H_DMI = self._validate_dmi(H_DMI)
|
|
57
|
+
if matrix_type not in ("sparse", "dense", "auto"):
|
|
58
|
+
raise ValueError("matrix_type must be 'sparse', 'dense', or 'auto'")
|
|
59
|
+
self.matrix_type = matrix_type
|
|
60
|
+
if matrix_type == "sparse":
|
|
61
|
+
self.H_E = sparse.csr_matrix(self.H_E)
|
|
62
|
+
self.H_DMI = tuple(
|
|
63
|
+
sparse.csr_matrix(component) for component in self.H_DMI
|
|
64
|
+
)
|
|
65
|
+
elif matrix_type == "dense":
|
|
66
|
+
self.H_E = (
|
|
67
|
+
self.H_E.toarray()
|
|
68
|
+
if sparse.issparse(self.H_E)
|
|
69
|
+
else np.asarray(self.H_E)
|
|
70
|
+
)
|
|
71
|
+
self.H_DMI = np.stack(
|
|
72
|
+
[
|
|
73
|
+
component.toarray()
|
|
74
|
+
if sparse.issparse(component)
|
|
75
|
+
else np.asarray(component)
|
|
76
|
+
for component in self.H_DMI
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
self.H_ext = np.zeros(3) if H_ext is None else np.asarray(H_ext)
|
|
80
|
+
self.H_FL = np.zeros(3) if H_FL is None else np.asarray(H_FL)
|
|
81
|
+
self.H_DL = np.zeros(3) if H_DL is None else np.asarray(H_DL)
|
|
82
|
+
self.H_perp = H_perp
|
|
83
|
+
self.H_para = H_para
|
|
84
|
+
self.phi_a = phi_a
|
|
85
|
+
self.alpha = alpha
|
|
86
|
+
self.method = method
|
|
87
|
+
self.io_foldername = io_foldername
|
|
88
|
+
self.io_filename = f"results_{method}" if io_filename is None else io_filename
|
|
89
|
+
self.io_compress = io_compress
|
|
90
|
+
self.io_screen = io_screen
|
|
91
|
+
|
|
92
|
+
def _validate_dmi(self, H_DMI: Optional[DMIField]) -> DMIField:
|
|
93
|
+
if H_DMI is None:
|
|
94
|
+
return np.zeros((3, self.N, self.N))
|
|
95
|
+
|
|
96
|
+
if sparse.issparse(H_DMI):
|
|
97
|
+
raise ValueError("sparse H_DMI must be a sequence of three matrices")
|
|
98
|
+
|
|
99
|
+
if isinstance(H_DMI, (list, tuple)) and any(
|
|
100
|
+
sparse.issparse(component) for component in H_DMI
|
|
101
|
+
):
|
|
102
|
+
if len(H_DMI) != 3:
|
|
103
|
+
raise ValueError("sparse H_DMI must contain three matrices")
|
|
104
|
+
for component in H_DMI:
|
|
105
|
+
if np.shape(component) != (self.N, self.N):
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"each H_DMI matrix must have shape ({self.N}, {self.N})"
|
|
108
|
+
)
|
|
109
|
+
return tuple(H_DMI)
|
|
110
|
+
|
|
111
|
+
H_DMI = np.asarray(H_DMI)
|
|
112
|
+
if H_DMI.shape != (3, self.N, self.N):
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"H_DMI must have shape (3, {self.N}, {self.N}), got {H_DMI.shape}"
|
|
115
|
+
)
|
|
116
|
+
return H_DMI
|
|
117
|
+
|
|
118
|
+
def _prepare_fields(self) -> Tuple[bool, Matrix, Optional[Matrix]]:
|
|
119
|
+
dmi_is_sparse = isinstance(self.H_DMI, tuple)
|
|
120
|
+
use_sparse = sparse.issparse(self.H_E) or dmi_is_sparse
|
|
121
|
+
|
|
122
|
+
if not use_sparse:
|
|
123
|
+
H_E = np.asarray(self.H_E)
|
|
124
|
+
H_E = (H_E + H_E.T) / 2
|
|
125
|
+
H_DMI = np.asarray(self.H_DMI)
|
|
126
|
+
H_DMI = (H_DMI - H_DMI.transpose(0, 2, 1)) / 2
|
|
127
|
+
if np.all(H_DMI == 0):
|
|
128
|
+
H_DMI = None
|
|
129
|
+
return False, H_E, H_DMI
|
|
130
|
+
|
|
131
|
+
H_E = sparse.csr_matrix(self.H_E)
|
|
132
|
+
H_E = ((H_E + H_E.T) / 2).tocsr()
|
|
133
|
+
H_E.eliminate_zeros()
|
|
134
|
+
H_E.sort_indices()
|
|
135
|
+
|
|
136
|
+
components = self.H_DMI if dmi_is_sparse else self.H_DMI
|
|
137
|
+
normalized_dmi = []
|
|
138
|
+
for component in components:
|
|
139
|
+
component = sparse.csr_matrix(component)
|
|
140
|
+
component = ((component - component.T) / 2).tocsr()
|
|
141
|
+
component.eliminate_zeros()
|
|
142
|
+
component.sort_indices()
|
|
143
|
+
normalized_dmi.append(component)
|
|
144
|
+
H_DMI = sparse.vstack(normalized_dmi, format="csr")
|
|
145
|
+
return True, H_E, H_DMI
|
|
146
|
+
|
|
147
|
+
def evolve(
|
|
148
|
+
self,
|
|
149
|
+
dt: float = 0.01,
|
|
150
|
+
max_iters: int = 1000,
|
|
151
|
+
restore_initial_state: bool = True,
|
|
152
|
+
) -> np.ndarray:
|
|
153
|
+
"""
|
|
154
|
+
param:
|
|
155
|
+
-----------------------------------------------
|
|
156
|
+
dt: float
|
|
157
|
+
time step per iter (ps)
|
|
158
|
+
max_iters: int
|
|
159
|
+
number of iteration
|
|
160
|
+
restore_initial_state: bool
|
|
161
|
+
If True, restore spins and velocities to their initial values after simulation
|
|
162
|
+
|
|
163
|
+
return:
|
|
164
|
+
-----------------------------------------------
|
|
165
|
+
record: (max_iters, N, 3)
|
|
166
|
+
1st dim: time
|
|
167
|
+
2nd dim: particle idx
|
|
168
|
+
3rd dim: spin (Sx,Sy,Sz)
|
|
169
|
+
"""
|
|
170
|
+
if max_iters <= 0:
|
|
171
|
+
raise ValueError("max_iters must be positive")
|
|
172
|
+
|
|
173
|
+
if restore_initial_state:
|
|
174
|
+
initial_spins = np.copy(self.lattice.spins)
|
|
175
|
+
initial_svels = np.copy(self.lattice.spin_velocities)
|
|
176
|
+
|
|
177
|
+
use_sparse, H_E, H_DMI = self._prepare_fields()
|
|
178
|
+
|
|
179
|
+
method = self.method
|
|
180
|
+
if method=="Euler":
|
|
181
|
+
_get_next_spin = _get_next_spin_euler_csr if use_sparse else _get_next_spin_euler
|
|
182
|
+
elif method=="RK2":
|
|
183
|
+
_get_next_spin = _get_next_spin_rk2_csr if use_sparse else _get_next_spin_rk2
|
|
184
|
+
elif method=="RK4":
|
|
185
|
+
_get_next_spin = _get_next_spin_rk4_csr if use_sparse else _get_next_spin_rk4
|
|
186
|
+
else:
|
|
187
|
+
raise ValueError("method must be 'Euler','RK2','RK4'")
|
|
188
|
+
|
|
189
|
+
Path(self.io_foldername).mkdir(parents=True, exist_ok=True)
|
|
190
|
+
|
|
191
|
+
record = np.zeros((max_iters, self.N, 3))
|
|
192
|
+
structure = self.lattice.structure
|
|
193
|
+
|
|
194
|
+
iters = tqdm(range(max_iters), desc='simulation') if self.io_screen else range(max_iters)
|
|
195
|
+
for n in iters:
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
spins = self.lattice.spins
|
|
199
|
+
svels = self.lattice.spin_velocities
|
|
200
|
+
if use_sparse:
|
|
201
|
+
next_spins, next_svels = _get_next_spin(
|
|
202
|
+
H_E.data, H_E.indices, H_E.indptr,
|
|
203
|
+
self.H_perp, self.H_para, self.phi_a,
|
|
204
|
+
H_DMI.data, H_DMI.indices, H_DMI.indptr, H_DMI.nnz > 0,
|
|
205
|
+
self.H_ext, self.H_FL, self.H_DL, self.alpha,
|
|
206
|
+
spins, svels, dt,
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
next_spins, next_svels = _get_next_spin(H_E = H_E,
|
|
210
|
+
H_perp = self.H_perp,
|
|
211
|
+
H_para = self.H_para,
|
|
212
|
+
phi_a= self.phi_a,
|
|
213
|
+
H_DMI = H_DMI,
|
|
214
|
+
H_ext = self.H_ext,
|
|
215
|
+
H_FL = self.H_FL,
|
|
216
|
+
H_DL = self.H_DL,
|
|
217
|
+
alpha = self.alpha,
|
|
218
|
+
spins = spins,
|
|
219
|
+
svels = svels,
|
|
220
|
+
dt = dt
|
|
221
|
+
)
|
|
222
|
+
record[n] = spins
|
|
223
|
+
self.lattice.spins = next_spins
|
|
224
|
+
self.lattice.spin_velocities = next_svels
|
|
225
|
+
|
|
226
|
+
if self.io_screen:
|
|
227
|
+
print(f"saving data to {self.io_foldername}/{self.io_filename}.h5 ...")
|
|
228
|
+
|
|
229
|
+
with h5py.File(f'{self.io_foldername}/{self.io_filename}.h5', 'w') as f:
|
|
230
|
+
dataset_options = {"chunks": (min(1000, max_iters), self.N, 3)}
|
|
231
|
+
if self.io_compress:
|
|
232
|
+
dataset_options["compression"] = "gzip"
|
|
233
|
+
f.create_dataset("spin data", data=record, **dataset_options)
|
|
234
|
+
f.create_dataset("structure", structure.shape, data = structure)
|
|
235
|
+
f.attrs['dt'] = dt
|
|
236
|
+
|
|
237
|
+
if restore_initial_state:
|
|
238
|
+
self.lattice.spins = initial_spins
|
|
239
|
+
self.lattice.spin_velocities = initial_svels
|
|
240
|
+
|
|
241
|
+
if self.io_screen:
|
|
242
|
+
print("simulation is done")
|
|
243
|
+
|
|
244
|
+
return record
|
|
245
|
+
|
|
246
|
+
@njit
|
|
247
|
+
def _get_next_spin_euler(H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha,
|
|
248
|
+
spins,svels, dt):
|
|
249
|
+
|
|
250
|
+
next_svels = calculate_spin_velocities_jit( H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha,
|
|
251
|
+
spins,svels)
|
|
252
|
+
next_spins = spins + dt*svels
|
|
253
|
+
next_spins = normalize(next_spins)
|
|
254
|
+
|
|
255
|
+
return next_spins, next_svels
|
|
256
|
+
|
|
257
|
+
@njit
|
|
258
|
+
def _get_next_spin_rk2(H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha,
|
|
259
|
+
spins,svels, dt):
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
#k2
|
|
263
|
+
spin2 = spins + dt/2*svels
|
|
264
|
+
svel2 = calculate_spin_velocities_jit(H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha,
|
|
265
|
+
spin2,svels)
|
|
266
|
+
|
|
267
|
+
next_spins = spins + dt/2*(svels+svel2)
|
|
268
|
+
next_spins = normalize(next_spins)
|
|
269
|
+
|
|
270
|
+
next_svels = calculate_spin_velocities_jit(H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha,
|
|
271
|
+
next_spins,svels)
|
|
272
|
+
|
|
273
|
+
return next_spins, next_svels
|
|
274
|
+
|
|
275
|
+
@njit
|
|
276
|
+
def _get_next_spin_rk4(H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha,
|
|
277
|
+
spins,svels, dt):
|
|
278
|
+
|
|
279
|
+
spins_i = spins.copy()
|
|
280
|
+
#k2
|
|
281
|
+
spin2 = spins_i + dt/2*svels
|
|
282
|
+
svel2 = calculate_spin_velocities_jit(H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha,
|
|
283
|
+
spin2,svels)
|
|
284
|
+
|
|
285
|
+
#k3
|
|
286
|
+
spin3 = spins_i + dt/2*svel2
|
|
287
|
+
svel3 = calculate_spin_velocities_jit(H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha,
|
|
288
|
+
spin3,svels)
|
|
289
|
+
|
|
290
|
+
#k4
|
|
291
|
+
spin4 = spins_i + dt*svel3
|
|
292
|
+
svel4 = calculate_spin_velocities_jit(H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha,
|
|
293
|
+
spin4,svels)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
next_spins = spins_i + dt/6*(svels+2*svel2+2*svel3+svel4)
|
|
298
|
+
next_spins = normalize(next_spins)
|
|
299
|
+
|
|
300
|
+
next_svels = calculate_spin_velocities_jit(H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha,
|
|
301
|
+
next_spins,svels)
|
|
302
|
+
|
|
303
|
+
return next_spins, next_svels
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@njit
|
|
307
|
+
def _get_next_spin_euler_csr(
|
|
308
|
+
H_E_data, H_E_indices, H_E_indptr, H_perp, H_para, phi_a,
|
|
309
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, has_DMI,
|
|
310
|
+
H_ext, H_FL, H_DL, alpha, spins, svels, dt,
|
|
311
|
+
):
|
|
312
|
+
next_svels = calculate_spin_velocities_csr_jit(
|
|
313
|
+
H_E_data, H_E_indices, H_E_indptr, H_perp, H_para, phi_a,
|
|
314
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, has_DMI,
|
|
315
|
+
H_ext, H_FL, H_DL, alpha, spins, svels,
|
|
316
|
+
)
|
|
317
|
+
next_spins = normalize(spins + dt * svels)
|
|
318
|
+
return next_spins, next_svels
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@njit
|
|
322
|
+
def _get_next_spin_rk2_csr(
|
|
323
|
+
H_E_data, H_E_indices, H_E_indptr, H_perp, H_para, phi_a,
|
|
324
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, has_DMI,
|
|
325
|
+
H_ext, H_FL, H_DL, alpha, spins, svels, dt,
|
|
326
|
+
):
|
|
327
|
+
spin2 = spins + dt / 2 * svels
|
|
328
|
+
svel2 = calculate_spin_velocities_csr_jit(
|
|
329
|
+
H_E_data, H_E_indices, H_E_indptr, H_perp, H_para, phi_a,
|
|
330
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, has_DMI,
|
|
331
|
+
H_ext, H_FL, H_DL, alpha, spin2, svels,
|
|
332
|
+
)
|
|
333
|
+
next_spins = normalize(spins + dt / 2 * (svels + svel2))
|
|
334
|
+
next_svels = calculate_spin_velocities_csr_jit(
|
|
335
|
+
H_E_data, H_E_indices, H_E_indptr, H_perp, H_para, phi_a,
|
|
336
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, has_DMI,
|
|
337
|
+
H_ext, H_FL, H_DL, alpha, next_spins, svels,
|
|
338
|
+
)
|
|
339
|
+
return next_spins, next_svels
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@njit
|
|
343
|
+
def _get_next_spin_rk4_csr(
|
|
344
|
+
H_E_data, H_E_indices, H_E_indptr, H_perp, H_para, phi_a,
|
|
345
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, has_DMI,
|
|
346
|
+
H_ext, H_FL, H_DL, alpha, spins, svels, dt,
|
|
347
|
+
):
|
|
348
|
+
spins_i = spins.copy()
|
|
349
|
+
spin2 = spins_i + dt / 2 * svels
|
|
350
|
+
svel2 = calculate_spin_velocities_csr_jit(
|
|
351
|
+
H_E_data, H_E_indices, H_E_indptr, H_perp, H_para, phi_a,
|
|
352
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, has_DMI,
|
|
353
|
+
H_ext, H_FL, H_DL, alpha, spin2, svels,
|
|
354
|
+
)
|
|
355
|
+
spin3 = spins_i + dt / 2 * svel2
|
|
356
|
+
svel3 = calculate_spin_velocities_csr_jit(
|
|
357
|
+
H_E_data, H_E_indices, H_E_indptr, H_perp, H_para, phi_a,
|
|
358
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, has_DMI,
|
|
359
|
+
H_ext, H_FL, H_DL, alpha, spin3, svels,
|
|
360
|
+
)
|
|
361
|
+
spin4 = spins_i + dt * svel3
|
|
362
|
+
svel4 = calculate_spin_velocities_csr_jit(
|
|
363
|
+
H_E_data, H_E_indices, H_E_indptr, H_perp, H_para, phi_a,
|
|
364
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, has_DMI,
|
|
365
|
+
H_ext, H_FL, H_DL, alpha, spin4, svels,
|
|
366
|
+
)
|
|
367
|
+
next_spins = normalize(spins_i + dt / 6 * (svels + 2 * svel2 + 2 * svel3 + svel4))
|
|
368
|
+
next_svels = calculate_spin_velocities_csr_jit(
|
|
369
|
+
H_E_data, H_E_indices, H_E_indptr, H_perp, H_para, phi_a,
|
|
370
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, has_DMI,
|
|
371
|
+
H_ext, H_FL, H_DL, alpha, next_spins, svels,
|
|
372
|
+
)
|
|
373
|
+
return next_spins, next_svels
|
llgs/__init__.py
ADDED
llgs/interactions.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Sparse interaction-matrix builders for periodic lattice topologies."""
|
|
2
|
+
|
|
3
|
+
from typing import Iterable, Sequence, Tuple, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy import sparse
|
|
7
|
+
|
|
8
|
+
from .lattice import Lattice_2D
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
CellOffset = Tuple[int, int]
|
|
12
|
+
ExchangeBond = Tuple[int, int, CellOffset, float]
|
|
13
|
+
DMIBond = Tuple[int, int, CellOffset, Sequence[float]]
|
|
14
|
+
Periodic = Union[bool, Tuple[bool, bool]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _periodic_axes(periodic: Periodic) -> Tuple[bool, bool]:
|
|
18
|
+
if isinstance(periodic, (bool, np.bool_)):
|
|
19
|
+
return bool(periodic), bool(periodic)
|
|
20
|
+
if len(periodic) != 2:
|
|
21
|
+
raise ValueError("periodic must be a bool or a pair of bools")
|
|
22
|
+
return bool(periodic[0]), bool(periodic[1])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _validate_bond(
|
|
26
|
+
lattice: Lattice_2D,
|
|
27
|
+
source_site: int,
|
|
28
|
+
target_site: int,
|
|
29
|
+
offset: CellOffset,
|
|
30
|
+
) -> Tuple[int, int]:
|
|
31
|
+
if not 0 <= source_site < lattice.n_site:
|
|
32
|
+
raise ValueError(f"source site {source_site} is outside the unit cell")
|
|
33
|
+
if not 0 <= target_site < lattice.n_site:
|
|
34
|
+
raise ValueError(f"target site {target_site} is outside the unit cell")
|
|
35
|
+
if len(offset) != 2 or any(int(value) != value for value in offset):
|
|
36
|
+
raise ValueError("bond cell offsets must contain two integers")
|
|
37
|
+
if source_site == target_site and tuple(offset) == (0, 0):
|
|
38
|
+
raise ValueError("a bond cannot connect a site to itself in the same cell")
|
|
39
|
+
return int(offset[0]), int(offset[1])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _particle_index(lattice: Lattice_2D, a: int, b: int, site: int) -> int:
|
|
43
|
+
return (a * lattice.n_b + b) * lattice.n_site + site
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _bond_indices(
|
|
47
|
+
lattice: Lattice_2D,
|
|
48
|
+
source_site: int,
|
|
49
|
+
target_site: int,
|
|
50
|
+
offset: CellOffset,
|
|
51
|
+
periodic: Tuple[bool, bool],
|
|
52
|
+
):
|
|
53
|
+
delta_a, delta_b = _validate_bond(
|
|
54
|
+
lattice, source_site, target_site, offset
|
|
55
|
+
)
|
|
56
|
+
for a in range(lattice.n_a):
|
|
57
|
+
for b in range(lattice.n_b):
|
|
58
|
+
target_a = a + delta_a
|
|
59
|
+
target_b = b + delta_b
|
|
60
|
+
|
|
61
|
+
if periodic[0]:
|
|
62
|
+
target_a %= lattice.n_a
|
|
63
|
+
elif not 0 <= target_a < lattice.n_a:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if periodic[1]:
|
|
67
|
+
target_b %= lattice.n_b
|
|
68
|
+
elif not 0 <= target_b < lattice.n_b:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
yield (
|
|
72
|
+
_particle_index(lattice, a, b, source_site),
|
|
73
|
+
_particle_index(lattice, target_a, target_b, target_site),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_exchange(
|
|
78
|
+
lattice: Lattice_2D,
|
|
79
|
+
bonds: Iterable[ExchangeBond],
|
|
80
|
+
periodic: Periodic = False,
|
|
81
|
+
) -> sparse.csr_matrix:
|
|
82
|
+
"""Build a symmetric CSR exchange matrix from unit-cell bond templates.
|
|
83
|
+
|
|
84
|
+
Each bond is ``(source_site, target_site, (delta_a, delta_b), coupling)``.
|
|
85
|
+
A template is repeated from every unit cell and its reverse matrix entry is
|
|
86
|
+
added automatically. With open boundaries, bonds leaving the lattice are
|
|
87
|
+
omitted. ``periodic`` may be one bool or ``(periodic_a, periodic_b)``.
|
|
88
|
+
"""
|
|
89
|
+
periodic_axes = _periodic_axes(periodic)
|
|
90
|
+
rows = []
|
|
91
|
+
columns = []
|
|
92
|
+
values = []
|
|
93
|
+
|
|
94
|
+
for source_site, target_site, offset, coupling in bonds:
|
|
95
|
+
for source, target in _bond_indices(
|
|
96
|
+
lattice, source_site, target_site, offset, periodic_axes
|
|
97
|
+
):
|
|
98
|
+
rows.extend((source, target))
|
|
99
|
+
columns.extend((target, source))
|
|
100
|
+
values.extend((coupling, coupling))
|
|
101
|
+
|
|
102
|
+
matrix = sparse.coo_matrix(
|
|
103
|
+
(values, (rows, columns)), shape=(lattice.N, lattice.N), dtype=float
|
|
104
|
+
).tocsr()
|
|
105
|
+
matrix.eliminate_zeros()
|
|
106
|
+
return matrix
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_dmi(
|
|
110
|
+
lattice: Lattice_2D,
|
|
111
|
+
bonds: Iterable[DMIBond],
|
|
112
|
+
periodic: Periodic = False,
|
|
113
|
+
) -> Tuple[sparse.csr_matrix, sparse.csr_matrix, sparse.csr_matrix]:
|
|
114
|
+
"""Build three antisymmetric CSR DMI matrices from bond templates.
|
|
115
|
+
|
|
116
|
+
Each bond is ``(source_site, target_site, (delta_a, delta_b), (Dx, Dy,
|
|
117
|
+
Dz))``. The reverse bond is assigned the negated DMI vector. Boundary
|
|
118
|
+
behavior matches :func:`build_exchange`.
|
|
119
|
+
"""
|
|
120
|
+
periodic_axes = _periodic_axes(periodic)
|
|
121
|
+
rows = []
|
|
122
|
+
columns = []
|
|
123
|
+
component_values = ([], [], [])
|
|
124
|
+
|
|
125
|
+
for source_site, target_site, offset, dmi_vector in bonds:
|
|
126
|
+
dmi_vector = np.asarray(dmi_vector, dtype=float)
|
|
127
|
+
if dmi_vector.shape != (3,):
|
|
128
|
+
raise ValueError("each DMI vector must have shape (3,)")
|
|
129
|
+
for source, target in _bond_indices(
|
|
130
|
+
lattice, source_site, target_site, offset, periodic_axes
|
|
131
|
+
):
|
|
132
|
+
rows.extend((source, target))
|
|
133
|
+
columns.extend((target, source))
|
|
134
|
+
for component, values in enumerate(component_values):
|
|
135
|
+
values.extend((dmi_vector[component], -dmi_vector[component]))
|
|
136
|
+
|
|
137
|
+
matrices = []
|
|
138
|
+
for values in component_values:
|
|
139
|
+
matrix = sparse.coo_matrix(
|
|
140
|
+
(values, (rows, columns)), shape=(lattice.N, lattice.N), dtype=float
|
|
141
|
+
).tocsr()
|
|
142
|
+
matrix.eliminate_zeros()
|
|
143
|
+
matrices.append(matrix)
|
|
144
|
+
return tuple(matrices)
|
llgs/lattice.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from typing import Literal, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def normalize(spins):
|
|
7
|
+
return spins / np.linalg.norm(spins, axis=1, keepdims=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Lattice_2D:
|
|
11
|
+
def __init__(self, n_a, n_b, n_site, r_a=None, r_b=None, r_site=None):
|
|
12
|
+
"""Create a two-dimensional spin lattice.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
n_a, n_b : int
|
|
17
|
+
Number of unit cells along the two lattice vectors.
|
|
18
|
+
n_site : int
|
|
19
|
+
Number of sites in each unit cell.
|
|
20
|
+
r_a, r_b : array-like of shape (2,), optional
|
|
21
|
+
Cartesian lattice vectors. Geometry is omitted when these and
|
|
22
|
+
``r_site`` are all ``None``.
|
|
23
|
+
r_site : array-like of shape (n_site, 2), optional
|
|
24
|
+
Fractional site coordinates within a unit cell. A row ``(u, v)``
|
|
25
|
+
places that site at ``u * r_a + v * r_b`` relative to the cell
|
|
26
|
+
origin. It must be supplied together with ``r_a`` and ``r_b``.
|
|
27
|
+
|
|
28
|
+
Notes
|
|
29
|
+
-----
|
|
30
|
+
The public arrays ``tags``, ``positions``, ``spins``,
|
|
31
|
+
``spin_velocities``, and ``structure`` contain the lattice state.
|
|
32
|
+
``structure`` has columns ``a, b, site`` and, when geometry is
|
|
33
|
+
provided, the Cartesian columns ``x, y``.
|
|
34
|
+
"""
|
|
35
|
+
geometry = (r_a, r_b, r_site)
|
|
36
|
+
if any(value is not None for value in geometry) and not all(
|
|
37
|
+
value is not None for value in geometry
|
|
38
|
+
):
|
|
39
|
+
raise ValueError("r_a, r_b, and r_site must be provided together")
|
|
40
|
+
|
|
41
|
+
self.n_site = n_site
|
|
42
|
+
self.n_a = n_a
|
|
43
|
+
self.n_b = n_b
|
|
44
|
+
self.N = n_site * n_a * n_b
|
|
45
|
+
|
|
46
|
+
a, b, site = np.meshgrid(
|
|
47
|
+
np.arange(n_a),
|
|
48
|
+
np.arange(n_b),
|
|
49
|
+
np.arange(n_site),
|
|
50
|
+
indexing="ij",
|
|
51
|
+
)
|
|
52
|
+
self.tags = np.stack(
|
|
53
|
+
(a.ravel(), b.ravel(), site.ravel()), axis=1
|
|
54
|
+
).astype(int)
|
|
55
|
+
self.positions = np.zeros((self.N, 2))
|
|
56
|
+
self.spins = np.zeros((self.N, 3))
|
|
57
|
+
self.spin_velocities = np.zeros((self.N, 3))
|
|
58
|
+
|
|
59
|
+
self.r_a = None
|
|
60
|
+
self.r_b = None
|
|
61
|
+
self.r_site = None
|
|
62
|
+
self.has_geometry = all(value is not None for value in geometry)
|
|
63
|
+
|
|
64
|
+
if self.has_geometry:
|
|
65
|
+
self.r_a = np.asarray(r_a)
|
|
66
|
+
self.r_b = np.asarray(r_b)
|
|
67
|
+
self.r_site = np.asarray(r_site)
|
|
68
|
+
if self.r_a.shape != (2,):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"r_a must be a (2,) array, but got shape {self.r_a.shape}."
|
|
71
|
+
)
|
|
72
|
+
if self.r_b.shape != (2,):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"r_b must be a (2,) array, but got shape {self.r_b.shape}."
|
|
75
|
+
)
|
|
76
|
+
if self.r_site.shape != (self.n_site, 2):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"r_site must be a ({self.n_site}, 2) array, "
|
|
79
|
+
f"but got shape {self.r_site.shape}."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
a, b, site = self.tags.T
|
|
83
|
+
site_coordinates = self.r_site[site]
|
|
84
|
+
self.positions = (
|
|
85
|
+
a[:, None] * self.r_a
|
|
86
|
+
+ b[:, None] * self.r_b
|
|
87
|
+
+ site_coordinates[:, 0, None] * self.r_a
|
|
88
|
+
+ site_coordinates[:, 1, None] * self.r_b
|
|
89
|
+
)
|
|
90
|
+
self.structure = np.column_stack((self.tags, self.positions))
|
|
91
|
+
else:
|
|
92
|
+
self.structure = self.tags.copy()
|
|
93
|
+
|
|
94
|
+
def output_lattice_structure(self, fn):
|
|
95
|
+
"""Write lattice data to a CSV file."""
|
|
96
|
+
output = np.column_stack((np.arange(self.N), self.structure))
|
|
97
|
+
columns = ["particle idx", "a", "b", "site"]
|
|
98
|
+
formats = ["%d"] * 4
|
|
99
|
+
if self.has_geometry:
|
|
100
|
+
columns.extend(("x", "y"))
|
|
101
|
+
formats.extend(("%.18e", "%.18e"))
|
|
102
|
+
np.savetxt(
|
|
103
|
+
fn,
|
|
104
|
+
output,
|
|
105
|
+
delimiter=",",
|
|
106
|
+
header=",".join(columns),
|
|
107
|
+
comments="",
|
|
108
|
+
fmt=formats,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def initialize_spin(self, condition_dict, perturb=0.0):
|
|
112
|
+
"""Initialize spins from NumPy expressions over lattice coordinates."""
|
|
113
|
+
condition_values = {
|
|
114
|
+
"a": self.tags[:, 0],
|
|
115
|
+
"b": self.tags[:, 1],
|
|
116
|
+
"site": self.tags[:, 2],
|
|
117
|
+
}
|
|
118
|
+
if self.has_geometry:
|
|
119
|
+
condition_values.update(
|
|
120
|
+
{
|
|
121
|
+
"x": self.positions[:, 0],
|
|
122
|
+
"y": self.positions[:, 1],
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
for cond_str, spin in condition_dict.items():
|
|
127
|
+
cond = np.asarray(
|
|
128
|
+
eval(cond_str, {"__builtins__": {}}, condition_values),
|
|
129
|
+
dtype=bool,
|
|
130
|
+
)
|
|
131
|
+
if cond.shape != (self.N,):
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"condition must produce shape ({self.N},), got {cond.shape}"
|
|
134
|
+
)
|
|
135
|
+
self.spins[cond] = spin
|
|
136
|
+
|
|
137
|
+
self.spins += perturb * np.random.normal(0, 1, (self.N, 3))
|
|
138
|
+
self.spins = normalize(self.spins)
|
|
139
|
+
|
|
140
|
+
def plot(
|
|
141
|
+
self,
|
|
142
|
+
arrowscale=0.3,
|
|
143
|
+
annotate_idx=False,
|
|
144
|
+
draw_unitcell=False,
|
|
145
|
+
display=True,
|
|
146
|
+
theme: Union[Literal["light", "dark"], dict] = "dark",
|
|
147
|
+
):
|
|
148
|
+
"""Plot this lattice and its current spin configuration.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
arrowscale : float, default 0.3
|
|
153
|
+
Scale applied to in-plane spin arrows.
|
|
154
|
+
annotate_idx : bool, default False
|
|
155
|
+
Label each lattice site with its particle index.
|
|
156
|
+
draw_unitcell : bool, default False
|
|
157
|
+
Draw the unit-cell boundary. Geometry must be available.
|
|
158
|
+
display : bool, default True
|
|
159
|
+
Show the figure with Matplotlib when true.
|
|
160
|
+
theme : {"light", "dark"} or dict, default "dark"
|
|
161
|
+
Built-in theme name or custom values overriding ``DARK_THEME``.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
tuple
|
|
166
|
+
Matplotlib ``(figure, axes)`` objects.
|
|
167
|
+
"""
|
|
168
|
+
from .plotting import _plot_lattice
|
|
169
|
+
|
|
170
|
+
return _plot_lattice(
|
|
171
|
+
self,
|
|
172
|
+
arrowscale=arrowscale,
|
|
173
|
+
annotate_idx=annotate_idx,
|
|
174
|
+
draw_unitcell=draw_unitcell,
|
|
175
|
+
display=display,
|
|
176
|
+
theme=theme,
|
|
177
|
+
)
|
llgs/plotting.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Visualization helpers for lattices and simulation results."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Literal, Tuple, Union
|
|
5
|
+
|
|
6
|
+
import matplotlib.animation as animation
|
|
7
|
+
import matplotlib.colors as colors
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
10
|
+
from matplotlib.axes import Axes
|
|
11
|
+
from matplotlib.figure import Figure
|
|
12
|
+
from matplotlib.patches import Polygon
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .lattice import Lattice_2D
|
|
16
|
+
from .read_results import ReadResult
|
|
17
|
+
|
|
18
|
+
DARK_THEME = {
|
|
19
|
+
"style": "dark_background",
|
|
20
|
+
"spin_cmap": "bwr",
|
|
21
|
+
"lattice_color": "yellow",
|
|
22
|
+
"unit_cell_edge_color": "blue",
|
|
23
|
+
"unit_cell_face_color": "lightblue",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
LIGHT_THEME = {
|
|
27
|
+
"style": "default",
|
|
28
|
+
"spin_cmap": colors.LinearSegmentedColormap.from_list(
|
|
29
|
+
"light_spin",
|
|
30
|
+
["#2563eb", "#1f2937", "#dc2626"],
|
|
31
|
+
),
|
|
32
|
+
"lattice_color": "black",
|
|
33
|
+
"unit_cell_edge_color": "navy",
|
|
34
|
+
"unit_cell_face_color": "lightblue",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Theme = Union[Literal["light", "dark"], dict]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _resolve_theme(theme: Theme) -> dict:
|
|
41
|
+
if theme == "dark":
|
|
42
|
+
return DARK_THEME
|
|
43
|
+
if theme == "light":
|
|
44
|
+
return LIGHT_THEME
|
|
45
|
+
if isinstance(theme, dict):
|
|
46
|
+
return {**DARK_THEME, **theme}
|
|
47
|
+
raise ValueError("theme must be 'light', 'dark', or a theme dictionary")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_cmap(cmap):
|
|
51
|
+
return cmap if isinstance(cmap, colors.Colormap) else plt.get_cmap(cmap)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _plot_lattice(
|
|
55
|
+
lattice: "Lattice_2D",
|
|
56
|
+
arrowscale: float = 0.3,
|
|
57
|
+
annotate_idx: bool = False,
|
|
58
|
+
draw_unitcell: bool = False,
|
|
59
|
+
display: bool = True,
|
|
60
|
+
theme: Theme = "dark",
|
|
61
|
+
) -> Tuple[Figure, Axes]:
|
|
62
|
+
"""Plot lattice positions and the current spin configuration."""
|
|
63
|
+
x = lattice.positions[:, 0]
|
|
64
|
+
y = lattice.positions[:, 1]
|
|
65
|
+
sx = lattice.spins[:, 0]
|
|
66
|
+
sy = lattice.spins[:, 1]
|
|
67
|
+
sz = lattice.spins[:, 2]
|
|
68
|
+
theme = _resolve_theme(theme)
|
|
69
|
+
|
|
70
|
+
with plt.style.context(theme["style"]):
|
|
71
|
+
fig, ax = plt.subplots()
|
|
72
|
+
cmap = _resolve_cmap(theme["spin_cmap"])
|
|
73
|
+
norm = colors.Normalize(vmin=-1, vmax=1)
|
|
74
|
+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
|
|
75
|
+
fig.colorbar(sm, ax=ax)
|
|
76
|
+
ax.scatter(x, y, c=theme["lattice_color"], s=2)
|
|
77
|
+
if np.linalg.norm(sx) == np.linalg.norm(sy) == 0 and np.linalg.norm(sz) != 0:
|
|
78
|
+
ax.scatter(x, y, c=sz, s=2, norm=norm, cmap=cmap)
|
|
79
|
+
elif np.linalg.norm(sx) != 0 or np.linalg.norm(sy) != 0:
|
|
80
|
+
ax.quiver(
|
|
81
|
+
x, y, sx * arrowscale, sy * arrowscale, sz, norm=norm, cmap=cmap
|
|
82
|
+
)
|
|
83
|
+
if annotate_idx:
|
|
84
|
+
for index, (x_position, y_position) in enumerate(zip(x, y)):
|
|
85
|
+
ax.annotate(
|
|
86
|
+
str(index),
|
|
87
|
+
(x_position, y_position),
|
|
88
|
+
xycoords="data",
|
|
89
|
+
xytext=(1.5, 1.5),
|
|
90
|
+
textcoords="offset points",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if draw_unitcell:
|
|
94
|
+
if not lattice.has_geometry:
|
|
95
|
+
raise ValueError("geometry is required to draw the unit cell")
|
|
96
|
+
origin = np.zeros(2)
|
|
97
|
+
opposite_corner = lattice.r_a + lattice.r_b
|
|
98
|
+
unit_cell = Polygon(
|
|
99
|
+
[origin, lattice.r_a, opposite_corner, lattice.r_b],
|
|
100
|
+
closed=True,
|
|
101
|
+
edgecolor=theme["unit_cell_edge_color"],
|
|
102
|
+
facecolor=theme["unit_cell_face_color"],
|
|
103
|
+
alpha=0.5,
|
|
104
|
+
)
|
|
105
|
+
ax.add_patch(unit_cell)
|
|
106
|
+
|
|
107
|
+
ax.set_aspect("equal")
|
|
108
|
+
ax.axis("off")
|
|
109
|
+
if display:
|
|
110
|
+
plt.show()
|
|
111
|
+
return fig, ax
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _animate(
|
|
115
|
+
result: "ReadResult",
|
|
116
|
+
period: int,
|
|
117
|
+
save_fn: Union[str, Path],
|
|
118
|
+
fps: int = 10,
|
|
119
|
+
display: bool = True,
|
|
120
|
+
theme: Theme = "dark",
|
|
121
|
+
) -> animation.FuncAnimation:
|
|
122
|
+
"""Animate saved spin data and write it to a GIF or video file."""
|
|
123
|
+
if result.structure.shape[1] < 5:
|
|
124
|
+
raise ValueError("structure must include x and y coordinates")
|
|
125
|
+
if period <= 0:
|
|
126
|
+
raise ValueError("period must be positive")
|
|
127
|
+
theme = _resolve_theme(theme)
|
|
128
|
+
|
|
129
|
+
with plt.style.context(theme["style"]):
|
|
130
|
+
fig, ax = plt.subplots()
|
|
131
|
+
line = ax.plot([], [])
|
|
132
|
+
cmap = _resolve_cmap(theme["spin_cmap"])
|
|
133
|
+
norm = colors.Normalize(vmin=-1, vmax=1)
|
|
134
|
+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
|
|
135
|
+
fig.colorbar(sm, ax=ax)
|
|
136
|
+
|
|
137
|
+
def initialize():
|
|
138
|
+
return line
|
|
139
|
+
|
|
140
|
+
def update_lattice(frame):
|
|
141
|
+
frame = min(frame * period, len(result.spin_datas) - 1)
|
|
142
|
+
x, y = result.structure[:, 3], result.structure[:, 4]
|
|
143
|
+
sx, sy, sz = result.spin_datas[frame].T
|
|
144
|
+
|
|
145
|
+
with plt.style.context(theme["style"]):
|
|
146
|
+
ax.clear()
|
|
147
|
+
ax.scatter(x, y, c=theme["lattice_color"], s=1)
|
|
148
|
+
ax.quiver(x, y, sx * 0.3, sy * 0.3, sz, norm=norm, cmap=cmap)
|
|
149
|
+
ax.set_aspect("equal")
|
|
150
|
+
ax.axis("off")
|
|
151
|
+
ax.set_title(f"time = {result.times[frame]:0.2f} ps")
|
|
152
|
+
return line
|
|
153
|
+
|
|
154
|
+
frame_count = max(1, len(result.times) // period)
|
|
155
|
+
output = animation.FuncAnimation(
|
|
156
|
+
fig,
|
|
157
|
+
update_lattice,
|
|
158
|
+
frames=frame_count,
|
|
159
|
+
init_func=initialize,
|
|
160
|
+
blit=True,
|
|
161
|
+
)
|
|
162
|
+
output.save(save_fn, fps=fps)
|
|
163
|
+
if display:
|
|
164
|
+
plt.show()
|
|
165
|
+
return output
|
llgs/read_results.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Literal, Union
|
|
2
|
+
|
|
3
|
+
import h5py
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
class ReadResult:
|
|
7
|
+
|
|
8
|
+
def __init__(self,fn):
|
|
9
|
+
'''
|
|
10
|
+
param:
|
|
11
|
+
-----------------------------
|
|
12
|
+
fn: hdf5 file name
|
|
13
|
+
|
|
14
|
+
attr:
|
|
15
|
+
-----------------------------
|
|
16
|
+
times: (T)
|
|
17
|
+
spin_datas: (T,N,3)
|
|
18
|
+
'''
|
|
19
|
+
with h5py.File(fn,'r') as f:
|
|
20
|
+
self.spin_datas = f['spin data'][()] # (T,N,3)
|
|
21
|
+
self.structure = f['structure'][()] # (N,5) a,b,site,x,y
|
|
22
|
+
self.times = np.arange(self.spin_datas.shape[0])*f.attrs['dt'] # (T)
|
|
23
|
+
|
|
24
|
+
def animate(
|
|
25
|
+
self,
|
|
26
|
+
period,
|
|
27
|
+
save_fn,
|
|
28
|
+
fps=10,
|
|
29
|
+
display=True,
|
|
30
|
+
theme: Union[Literal["light", "dark"], dict] = "dark",
|
|
31
|
+
):
|
|
32
|
+
"""Animate this result and save it to a GIF or video file.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
period : int
|
|
37
|
+
Number of simulation steps between animation frames.
|
|
38
|
+
save_fn : path-like
|
|
39
|
+
Output GIF or video filename.
|
|
40
|
+
fps : int, default 10
|
|
41
|
+
Saved animation frame rate.
|
|
42
|
+
display : bool, default True
|
|
43
|
+
Show the animation figure with Matplotlib when true.
|
|
44
|
+
theme : {"light", "dark"} or dict, default "dark"
|
|
45
|
+
Built-in theme name or custom values overriding ``DARK_THEME``.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
matplotlib.animation.FuncAnimation
|
|
50
|
+
The generated animation.
|
|
51
|
+
"""
|
|
52
|
+
from .plotting import _animate
|
|
53
|
+
|
|
54
|
+
return _animate(
|
|
55
|
+
self,
|
|
56
|
+
period=period,
|
|
57
|
+
save_fn=save_fn,
|
|
58
|
+
fps=fps,
|
|
59
|
+
display=display,
|
|
60
|
+
theme=theme,
|
|
61
|
+
)
|
llgs/spin_velocity.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from numba import njit
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@njit
|
|
6
|
+
def calculate_spin_velocities_jit(H_E,H_perp,H_para,phi_a,H_DMI,H_ext,H_FL,H_DL,alpha
|
|
7
|
+
,spins,svels):
|
|
8
|
+
# spins: (N,3) svels: (N,3)
|
|
9
|
+
gyro_magnetic_ratio = - 1.76085963023e-1
|
|
10
|
+
H_eff = H_eff_no_DMI(H_E,H_perp,H_para,phi_a,H_ext,H_FL,spins)
|
|
11
|
+
|
|
12
|
+
if not H_DMI is None:
|
|
13
|
+
H_eff += H_eff_DMI_term(H_DMI, spins)
|
|
14
|
+
|
|
15
|
+
next_svels = gyro_magnetic_ratio * (
|
|
16
|
+
np.cross(H_eff, spins)
|
|
17
|
+
+ np.cross(np.cross(H_DL, spins), spins)
|
|
18
|
+
+ alpha * np.cross(spins, svels)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return next_svels
|
|
22
|
+
|
|
23
|
+
@njit
|
|
24
|
+
def H_eff_no_DMI(H_E,H_perp,H_para,phi_a,H_ext,H_FL,spins):
|
|
25
|
+
H_eff = ( H_ext # applied field
|
|
26
|
+
+ H_FL # Field-like SOT
|
|
27
|
+
+ spins * np.array([2*H_para*np.cos(phi_a), 2*H_para*np.sin(phi_a), -2*H_perp]) # anisotropy
|
|
28
|
+
- H_E @ spins) # exchange
|
|
29
|
+
return H_eff
|
|
30
|
+
|
|
31
|
+
@njit
|
|
32
|
+
def H_eff_DMI_term(H_DMI, spins):
|
|
33
|
+
epsilon = np.zeros((3, 3, 3))
|
|
34
|
+
epsilon[0, 1, 2] = epsilon[1, 2, 0] = epsilon[2, 0, 1] = 1
|
|
35
|
+
epsilon[0, 2, 1] = epsilon[1, 0, 2] = epsilon[2, 1, 0] = -1
|
|
36
|
+
return (- H_DMI[0] @ spins @ epsilon[0]
|
|
37
|
+
- H_DMI[1] @ spins @ epsilon[1]
|
|
38
|
+
- H_DMI[2] @ spins @ epsilon[2]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@njit
|
|
43
|
+
def csr_matmul_spins(data, indices, indptr, spins):
|
|
44
|
+
"""Multiply a CSR matrix by an (N, 3) spin array."""
|
|
45
|
+
result = np.zeros((indptr.size - 1, 3), dtype=spins.dtype)
|
|
46
|
+
for row in range(indptr.size - 1):
|
|
47
|
+
for index in range(indptr[row], indptr[row + 1]):
|
|
48
|
+
result[row] += data[index] * spins[indices[index]]
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@njit
|
|
53
|
+
def calculate_spin_velocities_csr_jit(
|
|
54
|
+
H_E_data,
|
|
55
|
+
H_E_indices,
|
|
56
|
+
H_E_indptr,
|
|
57
|
+
H_perp,
|
|
58
|
+
H_para,
|
|
59
|
+
phi_a,
|
|
60
|
+
H_DMI_data,
|
|
61
|
+
H_DMI_indices,
|
|
62
|
+
H_DMI_indptr,
|
|
63
|
+
has_DMI,
|
|
64
|
+
H_ext,
|
|
65
|
+
H_FL,
|
|
66
|
+
H_DL,
|
|
67
|
+
alpha,
|
|
68
|
+
spins,
|
|
69
|
+
svels,
|
|
70
|
+
):
|
|
71
|
+
"""CSR equivalent of calculate_spin_velocities_jit."""
|
|
72
|
+
gyro_magnetic_ratio = -1.76085963023e-1
|
|
73
|
+
H_eff = (
|
|
74
|
+
H_ext
|
|
75
|
+
+ H_FL
|
|
76
|
+
+ spins
|
|
77
|
+
* np.array(
|
|
78
|
+
[2 * H_para * np.cos(phi_a), 2 * H_para * np.sin(phi_a), -2 * H_perp]
|
|
79
|
+
)
|
|
80
|
+
- csr_matmul_spins(H_E_data, H_E_indices, H_E_indptr, spins)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if has_DMI:
|
|
84
|
+
dmi_products = csr_matmul_spins(
|
|
85
|
+
H_DMI_data, H_DMI_indices, H_DMI_indptr, spins
|
|
86
|
+
)
|
|
87
|
+
n_spins = spins.shape[0]
|
|
88
|
+
epsilon = np.zeros((3, 3, 3))
|
|
89
|
+
epsilon[0, 1, 2] = epsilon[1, 2, 0] = epsilon[2, 0, 1] = 1
|
|
90
|
+
epsilon[0, 2, 1] = epsilon[1, 0, 2] = epsilon[2, 1, 0] = -1
|
|
91
|
+
for component in range(3):
|
|
92
|
+
start = component * n_spins
|
|
93
|
+
H_eff -= dmi_products[start : start + n_spins] @ epsilon[component]
|
|
94
|
+
|
|
95
|
+
return gyro_magnetic_ratio * (
|
|
96
|
+
np.cross(H_eff, spins)
|
|
97
|
+
+ np.cross(np.cross(H_DL, spins), spins)
|
|
98
|
+
+ alpha * np.cross(spins, svels)
|
|
99
|
+
)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llgs-simulation
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Spin-dynamics simulations using the Landau-Lifshitz-Gilbert-Slonczewski equation
|
|
5
|
+
Author: Ian Chang
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Ian Chang
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/ianchung621/LLGS-simulation
|
|
29
|
+
Project-URL: Repository, https://github.com/ianchung621/LLGS-simulation.git
|
|
30
|
+
Project-URL: Issues, https://github.com/ianchung621/LLGS-simulation/issues
|
|
31
|
+
Keywords: spintronics,LLGS,magnetism,spin dynamics,simulation
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
44
|
+
Requires-Python: >=3.9
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: LICENSE
|
|
47
|
+
Requires-Dist: h5py
|
|
48
|
+
Requires-Dist: matplotlib
|
|
49
|
+
Requires-Dist: numba
|
|
50
|
+
Requires-Dist: numpy
|
|
51
|
+
Requires-Dist: scipy
|
|
52
|
+
Requires-Dist: tqdm
|
|
53
|
+
Provides-Extra: test
|
|
54
|
+
Requires-Dist: pytest; extra == "test"
|
|
55
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
# LLGS Simulation
|
|
59
|
+
|
|
60
|
+
[](https://github.com/ianchung621/LLGS-simulation/actions/workflows/test.yml)
|
|
61
|
+
[](https://codecov.io/gh/ianchung621/LLGS-simulation)
|
|
62
|
+

|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
This package provides tools for simulating and analyzing spin dynamics using
|
|
66
|
+
the Landau-Lifshitz-Gilbert-Slonczewski (LLGS) equation on two-dimensional
|
|
67
|
+
lattices. It supports antiferromagnetic exchange, DMI, anisotropy, external
|
|
68
|
+
fields, and spin-orbit torque.
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
LLGS Simulation requires Python 3.9 or newer:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python -m venv .venv
|
|
76
|
+
source .venv/bin/activate
|
|
77
|
+
python -m pip install llgs-simulation
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Example Usage
|
|
81
|
+
|
|
82
|
+
Below is a step-by-step demonstration of how to use the repository to simulate spin dynamics and visualize the results.
|
|
83
|
+
|
|
84
|
+
### 1. Creating a Lattice Object
|
|
85
|
+
|
|
86
|
+
The `lattice.py` module allows you to define a spin lattice. Here's an example of creating a hexagonal lattice:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import numpy as np
|
|
90
|
+
|
|
91
|
+
from llgs import Lattice_2D
|
|
92
|
+
|
|
93
|
+
honeycomb = Lattice_2D(
|
|
94
|
+
n_a=10,
|
|
95
|
+
n_b=8,
|
|
96
|
+
n_site=2,
|
|
97
|
+
r_a=[np.sqrt(3), 0],
|
|
98
|
+
r_b=[0.5 * np.sqrt(3), 1.5],
|
|
99
|
+
r_site=[
|
|
100
|
+
[1 / 3, 1 / 3],
|
|
101
|
+
[2 / 3, 2 / 3],
|
|
102
|
+
],
|
|
103
|
+
)
|
|
104
|
+
honeycomb.plot(draw_unitcell=True)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+

|
|
108
|
+
|
|
109
|
+
### 2. Initializing Spins
|
|
110
|
+
|
|
111
|
+
Initialize the spins on the lattice to a desired configuration:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
zigzag_config = {
|
|
115
|
+
"b % 2 == 0": np.array([1, 0, 0]),
|
|
116
|
+
"b % 2 == 1": np.array([-1, 0, 0]),
|
|
117
|
+
}
|
|
118
|
+
honeycomb.initialize_spin(zigzag_config)
|
|
119
|
+
honeycomb.plot()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
### 3. Setting Up Parameters for Simulation
|
|
125
|
+
|
|
126
|
+
Build the exchange matrix from exact lattice bonds, then configure the
|
|
127
|
+
simulation. Each bond is `(source_site, target_site, cell_offset, coupling)`;
|
|
128
|
+
the reverse bond is added automatically.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from llgs import LLGS_Simulation_2D, build_exchange
|
|
132
|
+
|
|
133
|
+
J1 = -11.2 # exchange-matrix coefficient, Tesla
|
|
134
|
+
exchange_bonds = [
|
|
135
|
+
(0, 1, (0, 0), J1),
|
|
136
|
+
(1, 0, (0, 1), J1),
|
|
137
|
+
(1, 0, (1, 0), J1),
|
|
138
|
+
]
|
|
139
|
+
H_E = build_exchange(honeycomb, exchange_bonds)
|
|
140
|
+
H_ext = np.array([5.0, 5.0, 0.0])
|
|
141
|
+
|
|
142
|
+
sim = LLGS_Simulation_2D(
|
|
143
|
+
honeycomb,
|
|
144
|
+
H_E=H_E,
|
|
145
|
+
H_ext=H_ext,
|
|
146
|
+
alpha=0.1,
|
|
147
|
+
H_para=0.086,
|
|
148
|
+
H_perp=1.812,
|
|
149
|
+
io_foldername="Data",
|
|
150
|
+
io_filename="results_RK4",
|
|
151
|
+
method="RK4",
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`H_E` may be a dense `(N, N)` array or a SciPy sparse matrix. For sparse DMI,
|
|
156
|
+
pass `H_DMI` as a sequence of three sparse `(N, N)` matrices, one for each
|
|
157
|
+
Cartesian component. Dense DMI remains a `(3, N, N)` array. Use
|
|
158
|
+
`matrix_type="sparse"` to convert both fields to CSR matrices,
|
|
159
|
+
`matrix_type="dense"` to convert them to NumPy arrays, or the default
|
|
160
|
+
`matrix_type="auto"` to preserve the supplied representation.
|
|
161
|
+
|
|
162
|
+
### 4. Running the Simulation
|
|
163
|
+
|
|
164
|
+
Run the simulation for a specified number of steps and save the results:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
sim.evolve(dt=2e-4, max_iters=1000)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 5. Visualizing Spin Dynamics
|
|
171
|
+
|
|
172
|
+
The `read_result.py` module reads the simulation results and creates visualizations. Here is how you can animate the spin dynamics:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from llgs import ReadResult
|
|
176
|
+
|
|
177
|
+
results = ReadResult("Data/results_RK4.h5")
|
|
178
|
+
|
|
179
|
+
results.animate(period=50, save_fn="Data/movie.gif")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Example Output
|
|
183
|
+
|
|
184
|
+
Here is an example of how the animation might look:
|
|
185
|
+
|
|
186
|
+

|
|
187
|
+
|
|
188
|
+
### Additional Details
|
|
189
|
+
|
|
190
|
+
For a comprehensive explanation of the methods, equations, and parameters,
|
|
191
|
+
see the [example notebook](https://github.com/ianchung621/LLGS-simulation/blob/main/example.ipynb).
|
|
192
|
+
|
|
193
|
+
This notebook includes:
|
|
194
|
+
- Detailed descriptions of lattice construction.
|
|
195
|
+
- Explanation of the exchange field matrix and LLGS simulation.
|
|
196
|
+
- Mathematical derivations and parameter setups.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
llgs/LLGS_simulation.py,sha256=lXgFWoqvueRX85MsalzArGRT27O9WT--2_WM9-J-y0Y,14110
|
|
2
|
+
llgs/__init__.py,sha256=ZXaGJ0-5GBxfFFZ2KaIZfG9gFO0JbFKXYG95LVxPmd0,169
|
|
3
|
+
llgs/interactions.py,sha256=1gKLyVRgtEAymq88BDjLsF6ER5l5YBMl_OLfhAoyeSI,4931
|
|
4
|
+
llgs/lattice.py,sha256=jGV6wghJKzE6xmHQg4imXEnXbsC1qJwi4jVN03LFCQ0,6126
|
|
5
|
+
llgs/plotting.py,sha256=ouVE9Aw54R6a2fJ-wtMfErVqeNSUtAgGRcnAmzIkZ4g,5206
|
|
6
|
+
llgs/read_results.py,sha256=Vu6NJ2sAjOAUu8Q82ifTPf1qIYhRbswH8frgb1Rk1Rs,1661
|
|
7
|
+
llgs/spin_velocity.py,sha256=xuc1HkLPNQg7uWPKmjMCVbvoeqeM-55NzY-jhggYXdk,2914
|
|
8
|
+
llgs_simulation-0.1.0.dist-info/licenses/LICENSE,sha256=_fb8CcHtiob0C7tAYxiXmIw5rjS2-utwjsvmJW-su5Q,1066
|
|
9
|
+
llgs_simulation-0.1.0.dist-info/METADATA,sha256=GQAeF2cGWHCVg0NoxSPSGGTntS0nAhDluBNvUauIyR0,6781
|
|
10
|
+
llgs_simulation-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
11
|
+
llgs_simulation-0.1.0.dist-info/top_level.txt,sha256=ptJ0VXO0PUFXolrbCo5lTxRNzKNxCj5d5qxTfnlmTTE,5
|
|
12
|
+
llgs_simulation-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ian Chang
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
llgs
|