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.
- anisotropy/__init__.py +28 -0
- anisotropy/anisotropy.py +13 -0
- anisotropy/evaluation.py +70 -0
- anisotropy/model.py +200 -0
- anisotropy/predefined.py +202 -0
- anisotropy/py.typed +1 -0
- anisotropy/values.py +84 -0
- mag_material/__init__.py +3 -0
- mag_material/mag_material.py +231 -0
- mag_material/parameters.py +150 -0
- mag_material/py.typed +1 -0
- nmag/__init__.py +36 -0
- nmag/backends.py +493 -0
- nmag/checkpoint.py +327 -0
- nmag/config.py +174 -0
- nmag/demag/__init__.py +39 -0
- nmag/demag/bem_operator.py +149 -0
- nmag/demag/geometry.py +134 -0
- nmag/demag/lindholm.py +133 -0
- nmag/demag/lindholm_fast.py +463 -0
- nmag/demag/linear.py +489 -0
- nmag/dynamics/__init__.py +255 -0
- nmag/output.py +27 -0
- nmag/parallel.py +45 -0
- nmag/py.typed +1 -0
- nmag/resources.py +38 -0
- nmag/simulation/__init__.py +519 -0
- nmag/simulation/anisotropy/__init__.py +5 -0
- nmag/simulation/anisotropy/fields.py +56 -0
- nmag/simulation/anisotropy/materials.py +138 -0
- nmag/simulation/demag/__init__.py +1 -0
- nmag/simulation/demag/bem/__init__.py +11 -0
- nmag/simulation/demag/bem/diagnostics.py +73 -0
- nmag/simulation/demag/bem/dirichlet.py +85 -0
- nmag/simulation/demag/bem/hierarchical.py +74 -0
- nmag/simulation/demag/bem/operator.py +292 -0
- nmag/simulation/demag/fem/__init__.py +11 -0
- nmag/simulation/demag/fem/assembly.py +114 -0
- nmag/simulation/demag/fem/charges.py +38 -0
- nmag/simulation/demag/fem/geometry.py +261 -0
- nmag/simulation/demag/fields/__init__.py +11 -0
- nmag/simulation/demag/fields/auxiliary.py +186 -0
- nmag/simulation/demag/fields/probe.py +77 -0
- nmag/simulation/demag/fields/recovery.py +207 -0
- nmag/simulation/demag/solver.py +17 -0
- nmag/simulation/dynamics/__init__.py +92 -0
- nmag/simulation/dynamics/advance.py +199 -0
- nmag/simulation/dynamics/integrator.py +263 -0
- nmag/simulation/exchange/__init__.py +13 -0
- nmag/simulation/exchange/coefficients.py +185 -0
- nmag/simulation/exchange/fields.py +106 -0
- nmag/simulation/exchange/llg_rhs.py +218 -0
- nmag/simulation/fields/__init__.py +19 -0
- nmag/simulation/fields/arrays.py +111 -0
- nmag/simulation/fields/availability.py +188 -0
- nmag/simulation/fields/averages.py +293 -0
- nmag/simulation/fields/derived.py +164 -0
- nmag/simulation/fields/maxangle.py +163 -0
- nmag/simulation/fields/probes.py +106 -0
- nmag/simulation/implicit_dynamics.py +221 -0
- nmag/simulation/mesh/__init__.py +13 -0
- nmag/simulation/mesh/geometry.py +153 -0
- nmag/simulation/mesh/materials.py +299 -0
- nmag/simulation/mesh/probe.py +234 -0
- nmag/simulation/restart.py +103 -0
- nmag/simulation/support.py +224 -0
- nmag_python_3-0.0.2.dist-info/METADATA +157 -0
- nmag_python_3-0.0.2.dist-info/RECORD +147 -0
- nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
- nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
- nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
- nmesh/__init__.py +130 -0
- nmesh/backend.py +286 -0
- nmesh/geometry/__init__.py +52 -0
- nmesh/geometry/boolean_operations.py +157 -0
- nmesh/geometry/primitives.py +453 -0
- nmesh/geometry/transform.py +126 -0
- nmesh/io/__init__.py +50 -0
- nmesh/io/ascii.py +132 -0
- nmesh/io/legacy_nmesh_hdf5.py +318 -0
- nmesh/io/meshio_support.py +170 -0
- nmesh/mesh_generation.py +182 -0
- nmesh/mesh_io.py +227 -0
- nmesh/mesh_model.py +147 -0
- nmesh/mesh_utilities.py +79 -0
- nmesh/mesher/__init__.py +21 -0
- nmesh/mesher/driver.py +146 -0
- nmesh/mesher/meshing_defaults.py +252 -0
- nmesh/mesher/meshing_parameters.py +185 -0
- nmesh/mesher/parity.py +21 -0
- nmesh/mesher/parity_canonical.py +142 -0
- nmesh/mesher/parity_comparison.py +191 -0
- nmesh/mesher/parity_metrics.py +114 -0
- nmesh/mesher/periodic.py +97 -0
- nmesh/mesher/relaxation/__init__.py +14 -0
- nmesh/mesher/relaxation/_constants.py +20 -0
- nmesh/mesher/relaxation/_types.py +15 -0
- nmesh/mesher/relaxation/density.py +170 -0
- nmesh/mesher/relaxation/engine/__init__.py +18 -0
- nmesh/mesher/relaxation/engine/state.py +155 -0
- nmesh/mesher/relaxation/engine/steps.py +248 -0
- nmesh/mesher/relaxation/engine/topology.py +230 -0
- nmesh/mesher/relaxation/forces/__init__.py +96 -0
- nmesh/mesher/relaxation/forces/jit.py +102 -0
- nmesh/mesher/relaxation/forces/neighbors.py +186 -0
- nmesh/mesher/relaxation/forces/simplex.py +302 -0
- nmesh/mesher/relaxation/forces/summary.py +207 -0
- nmesh/mesher/relaxation/forces/types.py +92 -0
- nmesh/mesher/relaxation/geometry/__init__.py +6 -0
- nmesh/mesher/relaxation/geometry/builder.py +154 -0
- nmesh/mesher/relaxation/geometry/model.py +194 -0
- nmesh/mesher/relaxation/seeding/__init__.py +74 -0
- nmesh/mesher/relaxation/seeding/periodic.py +88 -0
- nmesh/mesher/relaxation/seeding/points.py +88 -0
- nmesh/mesher/relaxation/seeding/sampling.py +142 -0
- nmesh/mesher/relaxation/topology/__init__.py +297 -0
- nmesh/mesher/relaxation/topology/finalize.py +78 -0
- nmesh/mesher/relaxation/topology/recovery.py +310 -0
- nmesh/mesher/sectioned_config.py +70 -0
- nmesh/nmesh.py +99 -0
- nmesh/py.typed +1 -0
- nmesh/utils/__init__.py +33 -0
- nmesh/utils/array_list_utils.py +128 -0
- nmesh/utils/constants.py +22 -0
- nmesh/utils/timing_memory_utils.py +51 -0
- nmesh/utils/types.py +13 -0
- si/constants.py +49 -0
- si/physical.py +722 -0
- si/py.typed +1 -0
- simulation/__init__.py +1 -0
- simulation/clock.py +237 -0
- simulation/data_writer.py +273 -0
- simulation/data_writer_collection.py +267 -0
- simulation/hysteresis.py +74 -0
- simulation/hysteresis_runner.py +286 -0
- simulation/hysteresis_schedule.py +180 -0
- simulation/inference/__init__.py +3 -0
- simulation/inference/inference.py +95 -0
- simulation/py.typed +1 -0
- simulation/quantity.py +88 -0
- simulation/simulation_core.py +458 -0
- throttler/__init__.py +3 -0
- throttler/py.typed +1 -0
- throttler/throttler.py +55 -0
- when/__init__.py +3 -0
- when/py.typed +1 -0
- 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,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)
|