nmag-python-3 0.0.2__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.
Files changed (147) hide show
  1. anisotropy/__init__.py +28 -0
  2. anisotropy/anisotropy.py +13 -0
  3. anisotropy/evaluation.py +70 -0
  4. anisotropy/model.py +200 -0
  5. anisotropy/predefined.py +202 -0
  6. anisotropy/py.typed +1 -0
  7. anisotropy/values.py +84 -0
  8. mag_material/__init__.py +3 -0
  9. mag_material/mag_material.py +231 -0
  10. mag_material/parameters.py +150 -0
  11. mag_material/py.typed +1 -0
  12. nmag/__init__.py +36 -0
  13. nmag/backends.py +493 -0
  14. nmag/checkpoint.py +327 -0
  15. nmag/config.py +174 -0
  16. nmag/demag/__init__.py +39 -0
  17. nmag/demag/bem_operator.py +149 -0
  18. nmag/demag/geometry.py +134 -0
  19. nmag/demag/lindholm.py +133 -0
  20. nmag/demag/lindholm_fast.py +463 -0
  21. nmag/demag/linear.py +489 -0
  22. nmag/dynamics/__init__.py +255 -0
  23. nmag/output.py +27 -0
  24. nmag/parallel.py +45 -0
  25. nmag/py.typed +1 -0
  26. nmag/resources.py +38 -0
  27. nmag/simulation/__init__.py +519 -0
  28. nmag/simulation/anisotropy/__init__.py +5 -0
  29. nmag/simulation/anisotropy/fields.py +56 -0
  30. nmag/simulation/anisotropy/materials.py +138 -0
  31. nmag/simulation/demag/__init__.py +1 -0
  32. nmag/simulation/demag/bem/__init__.py +11 -0
  33. nmag/simulation/demag/bem/diagnostics.py +73 -0
  34. nmag/simulation/demag/bem/dirichlet.py +85 -0
  35. nmag/simulation/demag/bem/hierarchical.py +74 -0
  36. nmag/simulation/demag/bem/operator.py +292 -0
  37. nmag/simulation/demag/fem/__init__.py +11 -0
  38. nmag/simulation/demag/fem/assembly.py +114 -0
  39. nmag/simulation/demag/fem/charges.py +38 -0
  40. nmag/simulation/demag/fem/geometry.py +261 -0
  41. nmag/simulation/demag/fields/__init__.py +11 -0
  42. nmag/simulation/demag/fields/auxiliary.py +186 -0
  43. nmag/simulation/demag/fields/probe.py +77 -0
  44. nmag/simulation/demag/fields/recovery.py +207 -0
  45. nmag/simulation/demag/solver.py +17 -0
  46. nmag/simulation/dynamics/__init__.py +92 -0
  47. nmag/simulation/dynamics/advance.py +199 -0
  48. nmag/simulation/dynamics/integrator.py +263 -0
  49. nmag/simulation/exchange/__init__.py +13 -0
  50. nmag/simulation/exchange/coefficients.py +185 -0
  51. nmag/simulation/exchange/fields.py +106 -0
  52. nmag/simulation/exchange/llg_rhs.py +218 -0
  53. nmag/simulation/fields/__init__.py +19 -0
  54. nmag/simulation/fields/arrays.py +111 -0
  55. nmag/simulation/fields/availability.py +188 -0
  56. nmag/simulation/fields/averages.py +293 -0
  57. nmag/simulation/fields/derived.py +164 -0
  58. nmag/simulation/fields/maxangle.py +163 -0
  59. nmag/simulation/fields/probes.py +106 -0
  60. nmag/simulation/implicit_dynamics.py +221 -0
  61. nmag/simulation/mesh/__init__.py +13 -0
  62. nmag/simulation/mesh/geometry.py +153 -0
  63. nmag/simulation/mesh/materials.py +299 -0
  64. nmag/simulation/mesh/probe.py +234 -0
  65. nmag/simulation/restart.py +103 -0
  66. nmag/simulation/support.py +224 -0
  67. nmag_python_3-0.0.2.dist-info/METADATA +157 -0
  68. nmag_python_3-0.0.2.dist-info/RECORD +147 -0
  69. nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
  70. nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
  71. nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
  72. nmesh/__init__.py +130 -0
  73. nmesh/backend.py +286 -0
  74. nmesh/geometry/__init__.py +52 -0
  75. nmesh/geometry/boolean_operations.py +157 -0
  76. nmesh/geometry/primitives.py +453 -0
  77. nmesh/geometry/transform.py +126 -0
  78. nmesh/io/__init__.py +50 -0
  79. nmesh/io/ascii.py +132 -0
  80. nmesh/io/legacy_nmesh_hdf5.py +318 -0
  81. nmesh/io/meshio_support.py +170 -0
  82. nmesh/mesh_generation.py +182 -0
  83. nmesh/mesh_io.py +227 -0
  84. nmesh/mesh_model.py +147 -0
  85. nmesh/mesh_utilities.py +79 -0
  86. nmesh/mesher/__init__.py +21 -0
  87. nmesh/mesher/driver.py +146 -0
  88. nmesh/mesher/meshing_defaults.py +252 -0
  89. nmesh/mesher/meshing_parameters.py +185 -0
  90. nmesh/mesher/parity.py +21 -0
  91. nmesh/mesher/parity_canonical.py +142 -0
  92. nmesh/mesher/parity_comparison.py +191 -0
  93. nmesh/mesher/parity_metrics.py +114 -0
  94. nmesh/mesher/periodic.py +97 -0
  95. nmesh/mesher/relaxation/__init__.py +14 -0
  96. nmesh/mesher/relaxation/_constants.py +20 -0
  97. nmesh/mesher/relaxation/_types.py +15 -0
  98. nmesh/mesher/relaxation/density.py +170 -0
  99. nmesh/mesher/relaxation/engine/__init__.py +18 -0
  100. nmesh/mesher/relaxation/engine/state.py +155 -0
  101. nmesh/mesher/relaxation/engine/steps.py +248 -0
  102. nmesh/mesher/relaxation/engine/topology.py +230 -0
  103. nmesh/mesher/relaxation/forces/__init__.py +96 -0
  104. nmesh/mesher/relaxation/forces/jit.py +102 -0
  105. nmesh/mesher/relaxation/forces/neighbors.py +186 -0
  106. nmesh/mesher/relaxation/forces/simplex.py +302 -0
  107. nmesh/mesher/relaxation/forces/summary.py +207 -0
  108. nmesh/mesher/relaxation/forces/types.py +92 -0
  109. nmesh/mesher/relaxation/geometry/__init__.py +6 -0
  110. nmesh/mesher/relaxation/geometry/builder.py +154 -0
  111. nmesh/mesher/relaxation/geometry/model.py +194 -0
  112. nmesh/mesher/relaxation/seeding/__init__.py +74 -0
  113. nmesh/mesher/relaxation/seeding/periodic.py +88 -0
  114. nmesh/mesher/relaxation/seeding/points.py +88 -0
  115. nmesh/mesher/relaxation/seeding/sampling.py +142 -0
  116. nmesh/mesher/relaxation/topology/__init__.py +297 -0
  117. nmesh/mesher/relaxation/topology/finalize.py +78 -0
  118. nmesh/mesher/relaxation/topology/recovery.py +310 -0
  119. nmesh/mesher/sectioned_config.py +70 -0
  120. nmesh/nmesh.py +99 -0
  121. nmesh/py.typed +1 -0
  122. nmesh/utils/__init__.py +33 -0
  123. nmesh/utils/array_list_utils.py +128 -0
  124. nmesh/utils/constants.py +22 -0
  125. nmesh/utils/timing_memory_utils.py +51 -0
  126. nmesh/utils/types.py +13 -0
  127. si/constants.py +49 -0
  128. si/physical.py +722 -0
  129. si/py.typed +1 -0
  130. simulation/__init__.py +1 -0
  131. simulation/clock.py +237 -0
  132. simulation/data_writer.py +273 -0
  133. simulation/data_writer_collection.py +267 -0
  134. simulation/hysteresis.py +74 -0
  135. simulation/hysteresis_runner.py +286 -0
  136. simulation/hysteresis_schedule.py +180 -0
  137. simulation/inference/__init__.py +3 -0
  138. simulation/inference/inference.py +95 -0
  139. simulation/py.typed +1 -0
  140. simulation/quantity.py +88 -0
  141. simulation/simulation_core.py +458 -0
  142. throttler/__init__.py +3 -0
  143. throttler/py.typed +1 -0
  144. throttler/throttler.py +55 -0
  145. when/__init__.py +3 -0
  146. when/py.typed +1 -0
  147. when/when.py +416 -0
@@ -0,0 +1,519 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from collections.abc import Sequence
5
+ from typing import TYPE_CHECKING, Any, cast
6
+
7
+ import h5py
8
+ import numpy as np
9
+
10
+ import nmesh
11
+ from si.physical import SI
12
+ from simulation.clock import SimulationClock
13
+
14
+ from .. import backends as _backend_helpers
15
+ from .. import demag as _demag_helpers
16
+ from ..config import NmagConfig
17
+ from ..demag.bem_operator import BemOperatorStats
18
+ from ..demag.linear import ScipyLUFactorization
19
+ from ..dynamics import NodalMaterialCoefficients
20
+ from ..output import prepare_output_files
21
+ from . import fields as _field_helpers
22
+ from . import support as _support_helpers
23
+ from .anisotropy import SimulationAnisotropyMixin
24
+ from .anisotropy.materials import NodalAnisotropyGroup
25
+ from .demag.solver import SimulationDemagMixin
26
+ from .dynamics import SimulationDynamicsMixin
27
+ from .fields import SimulationFieldMixin
28
+ from .mesh import SimulationMeshMixin
29
+ from .restart import SimulationRestartMixin
30
+ from .support import (
31
+ ScalarFieldInput,
32
+ Vector,
33
+ VectorField,
34
+ VectorFieldInput,
35
+ _as_vector3,
36
+ _flatten_materials,
37
+ _normalise_m,
38
+ _scalar_nodal_field,
39
+ _si_unit,
40
+ _vector_nodal_field,
41
+ )
42
+
43
+ if TYPE_CHECKING:
44
+ from simulation.data_writer import DataWriter
45
+
46
+ _data_writer_class_cache: type[DataWriter] | None = None
47
+
48
+
49
+ def __getattr__(name: str) -> Any:
50
+ """Resolve compatibility exports moved to focused helper modules."""
51
+ try:
52
+ return getattr(_support_helpers, name)
53
+ except AttributeError:
54
+ pass
55
+ try:
56
+ return getattr(_demag_helpers, name)
57
+ except AttributeError:
58
+ pass
59
+ try:
60
+ return getattr(_backend_helpers, name)
61
+ except AttributeError:
62
+ pass
63
+ try:
64
+ return getattr(_field_helpers, name)
65
+ except AttributeError:
66
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
67
+
68
+
69
+ def _data_writer_class() -> type[DataWriter]:
70
+ global _data_writer_class_cache
71
+ if _data_writer_class_cache is None:
72
+ from simulation.data_writer import DataWriter
73
+
74
+ _data_writer_class_cache = DataWriter
75
+ return _data_writer_class_cache
76
+
77
+
78
+ class Simulation(
79
+ SimulationMeshMixin,
80
+ SimulationDemagMixin,
81
+ SimulationAnisotropyMixin,
82
+ SimulationFieldMixin,
83
+ SimulationDynamicsMixin,
84
+ SimulationRestartMixin,
85
+ ):
86
+ """Own a finite-element micromagnetic simulation and its output lifecycle.
87
+
88
+ A simulation is configured once, then receives a tetrahedral mesh, material
89
+ mapping, magnetization, and applied fields. Field calculations are lazy;
90
+ saving, probing, time advancement, or relaxation computes what is needed.
91
+
92
+ Args:
93
+ name: Base name for NDT, HDF5, and default checkpoint files. When
94
+ omitted, use ``config.default_name``.
95
+ phi_BEM: Reserved legacy HLib option. Non-null values are unsupported.
96
+ periodic_bc: Reserved legacy periodic-boundary option. Non-null values
97
+ are unsupported.
98
+ do_demag: Calculate demagnetization fields and energies when true.
99
+ do_sl_stt: Reserved for Slonczewski spin-transfer torque, which is not
100
+ implemented. Zhang-Li torque is configured through material
101
+ parameters and :meth:`set_current_density`.
102
+ config: Immutable output, acceleration, storage, and integrator policy.
103
+ Environment defaults are read only when this argument is omitted.
104
+
105
+ Raises:
106
+ NotImplementedError: If an unsupported legacy constructor option is
107
+ requested.
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ name: str | None = None,
113
+ phi_BEM: Any | None = None,
114
+ periodic_bc: Any | None = None,
115
+ do_demag: bool = True,
116
+ do_sl_stt: bool = False,
117
+ config: NmagConfig | None = None,
118
+ ) -> None:
119
+ if phi_BEM is not None:
120
+ raise NotImplementedError("phi_BEM/HLib support is not ported yet.")
121
+ if periodic_bc is not None:
122
+ raise NotImplementedError("Periodic boundary conditions are not ported yet.")
123
+ if do_sl_stt:
124
+ raise NotImplementedError(
125
+ "Spin-transfer torque using the Slonczewski model is not ported yet; "
126
+ "current-density (Zhang-Li) torque is supported."
127
+ )
128
+ self.config = NmagConfig.from_environment() if config is None else config
129
+ self.name = name or self.config.default_name
130
+ self.do_demag = do_demag
131
+ self.do_sl_stt = do_sl_stt
132
+ self.periodic_bc = periodic_bc
133
+ self.clock = SimulationClock()
134
+ data_writer_class = _data_writer_class()
135
+ ndt_filename = self.config.output_directory / f"{self.name}_dat.ndt"
136
+ h5_filename = self.config.output_directory / f"{self.name}_dat.h5"
137
+ prepare_output_files((ndt_filename, h5_filename), self.config.output_policy)
138
+ self.writer = data_writer_class(
139
+ ndt_filename=ndt_filename,
140
+ h5_filename=h5_filename,
141
+ append=self.config.output_policy == "append",
142
+ )
143
+ self.last_save_timings_seconds: dict[str, float] = {}
144
+ self.last_spatial_save_timings_seconds: dict[str, float] = {}
145
+ self.last_probe_timings_seconds: dict[str, float] = {}
146
+ self.last_subfield_average_timings_seconds: dict[str, float] = {}
147
+ self.last_maxangle_timings_seconds: dict[str, float] = {}
148
+ self.last_demag_solve_diagnostics: dict[str, float | int] = {}
149
+ self._last_bem_operator_stats: BemOperatorStats | None = None
150
+ self.last_demag_internal_vectors: dict[str, np.ndarray] = {}
151
+ self._active_subfield_array_timings: dict[str, float] | None = None
152
+
153
+ self.mesh: Any | None = None
154
+ self.mesh_unit_length: SI | None = None
155
+ self.region_name_list: list[str] = []
156
+ self.region_name_of_id: dict[int, str] = {}
157
+ self.region_id_of_name: dict[str, int] = {}
158
+ self.mats_of_region_name: dict[str, list[Any]] = {}
159
+ self.mat_of_mat_name: dict[str, Any] = {}
160
+ self.materials: list[Any] = []
161
+ self._fields: dict[str, Any] = {}
162
+ self._demag_cache_token: tuple[int, int] | None = None
163
+ self._demag_dipoles: tuple[np.ndarray, np.ndarray, float] | None = None
164
+ self._demag_nodal_cache: np.ndarray | None = None
165
+ self._demag_cell_cache_token: tuple[int, int] | None = None
166
+ self._demag_cell_field_cache: tuple[np.ndarray, np.ndarray] | None = None
167
+ self._demag_geometry_cache_token: int | None = None
168
+ self._demag_boundary_faces_cache: list[tuple[int, tuple[int, int, int]]] | None = None
169
+ self._demag_bem_cache: tuple[str, np.ndarray, Any, BemOperatorStats] | None = None
170
+ self._demag_fem_geometry_cache: tuple[str, Any, np.ndarray, np.ndarray] | None = None
171
+ self._demag_gauge_factorization_cache: ScipyLUFactorization | None = None
172
+ self._demag_dirichlet_factorization_cache: (
173
+ tuple[np.ndarray, np.ndarray, ScipyLUFactorization] | None
174
+ ) = None
175
+ self._demag_ms_values_cache: tuple[int, np.ndarray] | None = None
176
+ self._demag_volume_charge_scales_cache: tuple[int, np.ndarray] | None = None
177
+ self._nodal_ms_values_cache: tuple[int, np.ndarray] | None = None
178
+ self._demag_aux_cache_token: tuple[int, int] | None = None
179
+ self._demag_phi_cache: np.ndarray | None = None
180
+ self._demag_rho_cache: np.ndarray | None = None
181
+ self._demag_volumes_cache: np.ndarray | None = None
182
+ self._mesh_points_cache: tuple[int, np.ndarray] | None = None
183
+ self._mesh_bounds_cache: tuple[int, np.ndarray, np.ndarray, np.ndarray] | None = None
184
+ self._mesh_edge_cache: tuple[int, np.ndarray] | None = None
185
+ self._simplex_volume_cache: tuple[int, np.ndarray, np.ndarray] | None = None
186
+ self._volume_average_node_weights_cache: tuple[int, int, np.ndarray] | None = None
187
+ self._incident_cell_volume_sums_cache: tuple[int, int, int, np.ndarray] | None = None
188
+ self._exchange_cache_token: tuple[int, int] | None = None
189
+ self._exchange_nodal_cache: np.ndarray | None = None
190
+ self._exchange_spectral_bound_cache: tuple[int, float] | None = None
191
+ self._nodal_material_coefficients_cache: tuple[int, NodalMaterialCoefficients] | None = None
192
+ self._nodal_anisotropy_groups_cache: (
193
+ tuple[int, tuple[NodalAnisotropyGroup, ...]] | None
194
+ ) = None
195
+ self._anisotropy_fields_cache: tuple[
196
+ tuple[int, int],
197
+ np.ndarray,
198
+ np.ndarray,
199
+ ] | None = None
200
+ self._llg_affine_operator_cache: tuple[int, np.ndarray] | None = None
201
+ self._probe_geometry_cache_token: int | None = None
202
+ self._probe_tetrahedral_cache: (
203
+ tuple[
204
+ np.ndarray,
205
+ np.ndarray,
206
+ np.ndarray,
207
+ np.ndarray,
208
+ np.ndarray,
209
+ ]
210
+ | None
211
+ ) = None
212
+ self._subfield_array_cache: dict[str, np.ndarray] | None = None
213
+ self._subfield_average_cache: dict[tuple[str, str | None], object] | None = None
214
+ self._initialise_dynamics()
215
+
216
+ @property
217
+ def id(self) -> int:
218
+ return self.clock.id
219
+
220
+ @property
221
+ def stage(self) -> int:
222
+ return self.clock.stage
223
+
224
+ @property
225
+ def step(self) -> int:
226
+ return self.clock.step
227
+
228
+ @property
229
+ def stage_step(self) -> int:
230
+ return self.clock.stage_step
231
+
232
+ @property
233
+ def time(self) -> SI:
234
+ return self.clock.time
235
+
236
+ @property
237
+ def stage_time(self) -> SI:
238
+ return self.clock.stage_time
239
+
240
+ @property
241
+ def real_time(self) -> SI:
242
+ return self.clock.real_time
243
+
244
+ @property
245
+ def last_step_dt(self) -> SI:
246
+ return self.clock.last_step_dt_si
247
+
248
+ @property
249
+ def last_bem_operator_stats(self) -> BemOperatorStats | None:
250
+ """Return diagnostics for the currently cached boundary operator."""
251
+
252
+ return self._last_bem_operator_stats
253
+
254
+ def load_mesh(
255
+ self,
256
+ filename: str,
257
+ region_names_and_mag_mats: Sequence[tuple[str, Any]],
258
+ unit_length: SI,
259
+ do_reorder: bool = False,
260
+ manual_distribution: Any = None,
261
+ ) -> nmesh.Mesh:
262
+ """Load a mesh, scale it to metres, and map its regions to materials.
263
+
264
+ Args:
265
+ filename: Legacy Nmesh or Meshio-supported mesh file.
266
+ region_names_and_mag_mats: ``(region_name, material)`` pairs in
267
+ ascending mesh-region order, starting at region 1.
268
+ unit_length: Physical length represented by one mesh coordinate
269
+ unit.
270
+ do_reorder: Request legacy node reordering. Reordering is currently
271
+ unsupported by the mesh backend.
272
+ manual_distribution: Reserved legacy distributed-mesh mapping.
273
+
274
+ Returns:
275
+ The loaded and physically scaled mesh.
276
+
277
+ Raises:
278
+ RuntimeError: If a mesh has already been loaded.
279
+ ValueError: If mesh regions and configured materials do not match.
280
+ NotImplementedError: If reordering or manual distribution is
281
+ requested by an unsupported backend.
282
+ """
283
+ if self.mesh is not None:
284
+ raise RuntimeError("Mesh is already present.")
285
+
286
+ self.mesh = nmesh.load(
287
+ filename,
288
+ reorder=do_reorder,
289
+ distribute=manual_distribution is None,
290
+ )
291
+ self.mesh_unit_length = unit_length
292
+
293
+ scale = unit_length.in_units_of(_si_unit("m"))
294
+ if scale != 1.0:
295
+ self.mesh.scale_node_positions(scale)
296
+ self._fields["pin"] = np.ones(len(self.mesh.points), dtype=float)
297
+
298
+ if manual_distribution is not None:
299
+ self.mesh.set_vertex_distribution(manual_distribution)
300
+
301
+ self.region_name_list = [name for name, _ in region_names_and_mag_mats]
302
+ self.region_name_of_id = {
303
+ index: name for index, name in enumerate(self.region_name_list, start=1)
304
+ }
305
+ self.region_id_of_name = {name: index for index, name in self.region_name_of_id.items()}
306
+
307
+ mesh_region_ids = {int(region_id) for region_id in self.mesh.regions}
308
+ configured_region_ids = set(self.region_name_of_id)
309
+ if mesh_region_ids != configured_region_ids:
310
+ raise ValueError(
311
+ "Mesh material regions do not match the configured region list: "
312
+ f"mesh={sorted(mesh_region_ids)}, configured={sorted(configured_region_ids)}. "
313
+ "Materials must be supplied in ascending mesh-region order."
314
+ )
315
+
316
+ self.mats_of_region_name = {}
317
+ self.mat_of_mat_name = {}
318
+ for name, materials in region_names_and_mag_mats:
319
+ mats = cast(list[Any], materials) if isinstance(materials, list) else [materials]
320
+ self.mats_of_region_name[name] = mats
321
+ for material in mats:
322
+ self.mat_of_mat_name[material.name] = material
323
+
324
+ self.materials = _flatten_materials(region_names_and_mag_mats)
325
+ self._invalidate_demag(clear_geometry=True)
326
+ self._invalidate_integrator()
327
+ return self.mesh
328
+
329
+ def set_m(self, values: Vector | VectorField, subfieldname: str | None = None) -> None:
330
+ """Set and normalize the nodal magnetization direction field.
331
+
332
+ Args:
333
+ values: One three-component vector, one vector per mesh node, or a
334
+ callable receiving a physical node position in metres.
335
+ subfieldname: Reserved legacy material-subfield selector. Omit it;
336
+ material-specific magnetization degrees of freedom are not
337
+ implemented.
338
+
339
+ Raises:
340
+ RuntimeError: If no mesh has been loaded.
341
+ ValueError: If a vector is zero or the field shape is invalid.
342
+ NotImplementedError: If ``subfieldname`` is supplied.
343
+ """
344
+ if subfieldname is not None:
345
+ raise NotImplementedError("Material-specific m subfields are not ported yet.")
346
+
347
+ if callable(values):
348
+ if self.mesh is None:
349
+ raise RuntimeError("A mesh must be loaded before setting m from a function.")
350
+ self._fields["m"] = np.asarray(
351
+ [_normalise_m(values(point)) for point in self.mesh.points],
352
+ dtype=float,
353
+ )
354
+ self._invalidate_demag()
355
+ self._invalidate_integrator()
356
+ return
357
+
358
+ vector = _normalise_m(values)
359
+ point_count = len(self.mesh.points) if self.mesh is not None else 1
360
+ self._fields["m"] = np.tile(np.asarray(vector, dtype=float), (point_count, 1))
361
+ self._invalidate_demag()
362
+ self._invalidate_integrator()
363
+
364
+ def set_H_ext(self, values: Vector, unit: SI | None = None) -> None:
365
+ """Set the homogeneous applied magnetic field.
366
+
367
+ Args:
368
+ values: Three field components. When ``unit`` is omitted, values
369
+ may be SI quantities; otherwise they are interpreted in
370
+ ``unit``.
371
+ unit: Unit compatible with A/m.
372
+ """
373
+ self._fields["H_ext"] = np.asarray(_as_vector3(values, unit=unit), dtype=float)
374
+ self._invalidate_demag()
375
+ self._invalidate_integrator()
376
+
377
+ def set_pinning(self, values: ScalarFieldInput) -> None:
378
+ """Set the nodal multiplier for the complete magnetization derivative.
379
+
380
+ Args:
381
+ values: Uniform scalar, one value per mesh node, or a callable
382
+ receiving physical node positions in metres. Zero pins a node,
383
+ one leaves it free, and other finite values scale its rate.
384
+
385
+ Raises:
386
+ RuntimeError: If no mesh has been loaded.
387
+ ValueError: If values are non-finite or have the wrong shape.
388
+ """
389
+ if self.mesh is None:
390
+ raise RuntimeError("A mesh must be loaded before setting pinning.")
391
+ self._fields["pin"] = _scalar_nodal_field(
392
+ values,
393
+ self.mesh.points,
394
+ fieldname="pinning",
395
+ )
396
+ self._invalidate_integrator()
397
+
398
+ def set_current_density(
399
+ self,
400
+ values: VectorFieldInput,
401
+ unit: SI | None = None,
402
+ ) -> None:
403
+ """Set the current-density field used by Zhang-Li spin-transfer torque.
404
+
405
+ Args:
406
+ values: Uniform vector, nodal vectors, or a callable receiving
407
+ physical node positions in metres.
408
+ unit: Unit compatible with A/m². Required for ordinary numeric
409
+ vectors unless values already carry SI dimensions.
410
+
411
+ Raises:
412
+ RuntimeError: If no mesh has been loaded.
413
+ ValueError: If the field shape or dimensions are invalid.
414
+ """
415
+ if self.mesh is None:
416
+ raise RuntimeError("A mesh must be loaded before setting current density.")
417
+ self._fields["current_density"] = _vector_nodal_field(
418
+ values,
419
+ self.mesh.points,
420
+ unit=unit,
421
+ expected_unit=_si_unit("A/m^2"),
422
+ fieldname="current_density",
423
+ )
424
+ self._invalidate_integrator()
425
+
426
+ def save_data(
427
+ self, fields: str | list[str] | None = None, avoid_same_step: bool = False
428
+ ) -> None:
429
+ """Append averaged data and optionally save spatial fields.
430
+
431
+ Args:
432
+ fields: ``None`` for averages only, ``"all"`` for every available
433
+ field, or a list of field names to store spatially in HDF5.
434
+ avoid_same_step: Skip the save when this integrator step was already
435
+ written. Primarily used by scheduled actions.
436
+ """
437
+ if avoid_same_step and self.step == self.writer._last_saved_step:
438
+ return
439
+ self.clock.id += 1
440
+ with self._subfield_array_cache_scope(), self._subfield_average_cache_scope():
441
+ self.writer.save(self, fields=fields, avoid_same_step=avoid_same_step)
442
+ self.last_save_timings_seconds = dict(self.writer.last_save_timings_seconds)
443
+
444
+ def save_spatial_fields(self, filename: str, fieldnames: list[str]) -> None:
445
+ """Write selected spatial fields and mesh points to an HDF5 file.
446
+
447
+ Existing datasets with the same field names are replaced inside the
448
+ file; unrelated groups and datasets are retained.
449
+
450
+ Args:
451
+ filename: Destination HDF5 path.
452
+ fieldnames: Available field names such as ``m`` or ``H_demag``.
453
+ """
454
+ timings: dict[str, float] = {}
455
+ total_started = time.perf_counter()
456
+ try:
457
+ with h5py.File(filename, "a") as h5:
458
+ if self.mesh is not None:
459
+ started = time.perf_counter()
460
+ mesh_group = h5.require_group("mesh")
461
+ if "points" in mesh_group:
462
+ del mesh_group["points"]
463
+ mesh_group.create_dataset("points", data=np.asarray(self.mesh.points))
464
+ timings["mesh_points"] = time.perf_counter() - started
465
+
466
+ fields_group = h5.require_group("fields")
467
+ for fieldname in fieldnames:
468
+ field_started = time.perf_counter()
469
+ started = time.perf_counter()
470
+ data = self._subfield_array(fieldname)
471
+ timings[f"field:{fieldname}:compute"] = time.perf_counter() - started
472
+
473
+ started = time.perf_counter()
474
+ if fieldname in fields_group:
475
+ del fields_group[fieldname]
476
+ fields_group.create_dataset(fieldname, data=data)
477
+ timings[f"field:{fieldname}:write"] = time.perf_counter() - started
478
+ timings[f"field:{fieldname}:total"] = time.perf_counter() - field_started
479
+ finally:
480
+ timings["total"] = time.perf_counter() - total_started
481
+ self.last_spatial_save_timings_seconds = dict(sorted(timings.items()))
482
+
483
+ def reset_probe_timings(self) -> None:
484
+ """Reset passive probe timing counters used by parity/profile tools."""
485
+ self.last_probe_timings_seconds = {}
486
+
487
+ def probe_subfield(
488
+ self,
489
+ subfieldname: str,
490
+ pos: Sequence[float],
491
+ unit: SI | None = None,
492
+ ) -> Any:
493
+ """Probe a field at a physical position.
494
+
495
+ Args:
496
+ subfieldname: Field to sample.
497
+ pos: Three coordinates in metres.
498
+ unit: Reserved compatibility argument. Returned values use the
499
+ field's standard SI representation.
500
+
501
+ Returns:
502
+ Interpolated SI components, or ``None`` when the position is
503
+ outside the mesh.
504
+ """
505
+ return self.probe_subfield_siv(subfieldname, pos, unit=unit)
506
+
507
+ def save_mesh(self, filename: str) -> None:
508
+ """Save the currently loaded, physically scaled mesh.
509
+
510
+ Args:
511
+ filename: Destination path. The suffix selects Nmesh HDF5, Nmesh
512
+ ASCII, or a Meshio-supported format.
513
+
514
+ Raises:
515
+ RuntimeError: If no mesh has been loaded.
516
+ """
517
+ if self.mesh is None:
518
+ raise RuntimeError("No mesh has been loaded.")
519
+ nmesh.save(self.mesh, filename)
@@ -0,0 +1,5 @@
1
+ """Material anisotropy fields for the public simulation."""
2
+
3
+ from .fields import SimulationAnisotropyMixin
4
+
5
+ __all__ = ["SimulationAnisotropyMixin"]
@@ -0,0 +1,56 @@
1
+ """Evaluate anisotropy energy density and effective field."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import numpy as np
8
+
9
+ from anisotropy import evaluate_energy_and_gradient
10
+
11
+ from ..support import MU0
12
+ from .materials import SimulationAnisotropyMaterialMixin
13
+
14
+
15
+ class SimulationAnisotropyMixin(
16
+ SimulationAnisotropyMaterialMixin,
17
+ ):
18
+ if TYPE_CHECKING:
19
+ _anisotropy_fields_cache: tuple[tuple[int, int], np.ndarray, np.ndarray] | None
20
+
21
+ def __getattr__(self, name: str) -> Any: ...
22
+
23
+ def _get_anisotropy_fields(self) -> tuple[np.ndarray, np.ndarray]:
24
+ m = np.asarray(self._fields["m"], dtype=np.float64)
25
+ token = (id(self._require_mesh().raw_mesh), id(self._fields["m"]))
26
+ cached = self._anisotropy_fields_cache
27
+ if cached is not None and cached[0] == token:
28
+ return cached[1], cached[2]
29
+
30
+ field = np.zeros_like(m)
31
+ energy = np.zeros(len(m), dtype=np.float64)
32
+ for group in self._nodal_anisotropy_groups():
33
+ if group.model is None:
34
+ continue
35
+ nodes = group.nodes
36
+ group_m = m[nodes]
37
+ group_energy, gradient = evaluate_energy_and_gradient(group.model, group_m)
38
+ energy[nodes] = group_energy
39
+ if group.saturation_magnetisation != 0.0:
40
+ raw_field = -gradient / (MU0 * group.saturation_magnetisation)
41
+ norm_squared = np.einsum("ij,ij->i", group_m, group_m)
42
+ radial_scale = np.divide(
43
+ np.einsum("ij,ij->i", raw_field, group_m),
44
+ norm_squared,
45
+ out=np.zeros(len(group_m), dtype=np.float64),
46
+ where=norm_squared > 0.0,
47
+ )
48
+ field[nodes] = raw_field - radial_scale[:, np.newaxis] * group_m
49
+
50
+ if not np.all(np.isfinite(field)) or not np.all(np.isfinite(energy)):
51
+ raise FloatingPointError("Anisotropy evaluation produced non-finite fields.")
52
+ self._anisotropy_fields_cache = (token, field, energy)
53
+ return field, energy
54
+
55
+ def _anisotropy_is_zero_by_construction(self) -> bool:
56
+ return all(getattr(material, "anisotropy", None) is None for material in self.materials)