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.
@@ -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
@@ -0,0 +1,4 @@
1
+ from .lattice import Lattice_2D
2
+ from .LLGS_simulation import LLGS_Simulation_2D
3
+ from .read_results import ReadResult
4
+ from .interactions import build_dmi, build_exchange
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
+ [![Tests](https://github.com/ianchung621/LLGS-simulation/actions/workflows/test.yml/badge.svg)](https://github.com/ianchung621/LLGS-simulation/actions/workflows/test.yml)
61
+ [![Coverage](https://codecov.io/gh/ianchung621/LLGS-simulation/branch/main/graph/badge.svg)](https://codecov.io/gh/ianchung621/LLGS-simulation)
62
+ ![Python](https://img.shields.io/badge/python-3.9--3.13-blue)
63
+ ![OS](https://img.shields.io/badge/OS-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey)
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
+ ![Honeycomb lattice](https://raw.githubusercontent.com/ianchung621/LLGS-simulation/main/doc/lattice.png)
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
+ ![Initialized spins](https://raw.githubusercontent.com/ianchung621/LLGS-simulation/main/doc/spin_initialized.png)
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
+ ![Spin dynamics animation](https://raw.githubusercontent.com/ianchung621/LLGS-simulation/main/doc/spin_animation.gif)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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