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
anisotropy/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .evaluation import (
|
|
2
|
+
anisotropy_signature_values,
|
|
3
|
+
evaluate_energy_and_gradient,
|
|
4
|
+
evaluate_energy_density,
|
|
5
|
+
evaluate_energy_gradient,
|
|
6
|
+
)
|
|
7
|
+
from .model import (
|
|
8
|
+
AnisotropyModel,
|
|
9
|
+
EnergyDensity,
|
|
10
|
+
EnergyFunction,
|
|
11
|
+
PredefinedAnisotropy,
|
|
12
|
+
want_anisotropy,
|
|
13
|
+
)
|
|
14
|
+
from .predefined import cubic_anisotropy, uniaxial_anisotropy
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AnisotropyModel",
|
|
18
|
+
"EnergyDensity",
|
|
19
|
+
"EnergyFunction",
|
|
20
|
+
"PredefinedAnisotropy",
|
|
21
|
+
"anisotropy_signature_values",
|
|
22
|
+
"cubic_anisotropy",
|
|
23
|
+
"evaluate_energy_density",
|
|
24
|
+
"evaluate_energy_and_gradient",
|
|
25
|
+
"evaluate_energy_gradient",
|
|
26
|
+
"uniaxial_anisotropy",
|
|
27
|
+
"want_anisotropy",
|
|
28
|
+
]
|
anisotropy/anisotropy.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Compatibility exports for the focused anisotropy modules."""
|
|
2
|
+
|
|
3
|
+
from .model import PredefinedAnisotropy, want_anisotropy
|
|
4
|
+
from .predefined import cubic_anisotropy, uniaxial_anisotropy
|
|
5
|
+
from .values import _normalize
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"PredefinedAnisotropy",
|
|
9
|
+
"_normalize",
|
|
10
|
+
"cubic_anisotropy",
|
|
11
|
+
"uniaxial_anisotropy",
|
|
12
|
+
"want_anisotropy",
|
|
13
|
+
]
|
anisotropy/evaluation.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Evaluate arbitrary anisotropy models and build compatibility samples."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .model import AnisotropyModel, FloatArray, PredefinedAnisotropy
|
|
8
|
+
from .values import (
|
|
9
|
+
ArrayLike,
|
|
10
|
+
_callable_energy,
|
|
11
|
+
_finite_difference_gradient,
|
|
12
|
+
_magnetisation_array,
|
|
13
|
+
_normalize,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def evaluate_energy_density(model: AnisotropyModel, magnetisation: ArrayLike) -> FloatArray:
|
|
18
|
+
values, _scalar = _magnetisation_array(magnetisation)
|
|
19
|
+
if model is None:
|
|
20
|
+
return np.zeros(len(values), dtype=np.float64)
|
|
21
|
+
if isinstance(model, PredefinedAnisotropy):
|
|
22
|
+
return np.asarray(model.energy_density(values), dtype=np.float64)
|
|
23
|
+
return _callable_energy(model, values)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def evaluate_energy_gradient(model: AnisotropyModel, magnetisation: ArrayLike) -> FloatArray:
|
|
27
|
+
values, _scalar = _magnetisation_array(magnetisation)
|
|
28
|
+
if model is None:
|
|
29
|
+
return np.zeros_like(values)
|
|
30
|
+
if isinstance(model, PredefinedAnisotropy):
|
|
31
|
+
return np.asarray(model.energy_gradient(values), dtype=np.float64)
|
|
32
|
+
return _finite_difference_gradient(model, values)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def evaluate_energy_and_gradient(
|
|
36
|
+
model: AnisotropyModel,
|
|
37
|
+
magnetisation: ArrayLike,
|
|
38
|
+
) -> tuple[FloatArray, FloatArray]:
|
|
39
|
+
values, _scalar = _magnetisation_array(magnetisation)
|
|
40
|
+
if model is None:
|
|
41
|
+
return np.zeros(len(values), dtype=np.float64), np.zeros_like(values)
|
|
42
|
+
if isinstance(model, PredefinedAnisotropy):
|
|
43
|
+
return model.energy_and_gradient(values)
|
|
44
|
+
return _callable_energy(model, values), _finite_difference_gradient(model, values)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def anisotropy_signature_values(
|
|
48
|
+
model: AnisotropyModel,
|
|
49
|
+
order: int | None,
|
|
50
|
+
) -> FloatArray:
|
|
51
|
+
if model is None:
|
|
52
|
+
return np.asarray([0.0], dtype=np.float64)
|
|
53
|
+
probes = np.asarray(
|
|
54
|
+
[
|
|
55
|
+
[1.0, 0.0, 0.0],
|
|
56
|
+
[0.0, 1.0, 0.0],
|
|
57
|
+
[0.0, 0.0, 1.0],
|
|
58
|
+
_normalize([1.0, 1.0, 1.0]),
|
|
59
|
+
_normalize([1.0, 2.0, 3.0]),
|
|
60
|
+
],
|
|
61
|
+
dtype=np.float64,
|
|
62
|
+
)
|
|
63
|
+
energy, gradient = evaluate_energy_and_gradient(model, probes)
|
|
64
|
+
return np.concatenate(
|
|
65
|
+
(
|
|
66
|
+
np.asarray([float(order or 0)], dtype=np.float64),
|
|
67
|
+
energy,
|
|
68
|
+
np.ravel(gradient),
|
|
69
|
+
)
|
|
70
|
+
)
|
anisotropy/model.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Typed anisotropy models and numerical evaluation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TypeAlias, cast
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from si.physical import SI
|
|
12
|
+
|
|
13
|
+
from .values import (
|
|
14
|
+
ArrayLike,
|
|
15
|
+
FloatArray,
|
|
16
|
+
_callable_energy,
|
|
17
|
+
_finite_difference_gradient,
|
|
18
|
+
_magnetisation_array,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
EnergyDensity: TypeAlias = float | SI
|
|
22
|
+
EnergyFunction: TypeAlias = Callable[[ArrayLike], EnergyDensity]
|
|
23
|
+
GradientFunction: TypeAlias = Callable[[FloatArray], FloatArray]
|
|
24
|
+
VectorizedEnergyFunction: TypeAlias = Callable[[FloatArray], FloatArray]
|
|
25
|
+
EnergyGradientFunction: TypeAlias = Callable[[FloatArray], tuple[FloatArray, FloatArray]]
|
|
26
|
+
AnisotropyStringifier: TypeAlias = Callable[["PredefinedAnisotropy"], str]
|
|
27
|
+
AnisotropyModel: TypeAlias = "PredefinedAnisotropy | EnergyFunction | None"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class PredefinedAnisotropy:
|
|
32
|
+
"""An anisotropy energy model with optional analytic vectorized evaluation.
|
|
33
|
+
|
|
34
|
+
Instances returned by :func:`uniaxial_anisotropy` and
|
|
35
|
+
:func:`cubic_anisotropy` can be added, subtracted, negated, and multiplied by
|
|
36
|
+
scalar coefficients before being assigned to :class:`nmag.MagMaterial`.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
function: EnergyFunction | None = None
|
|
40
|
+
order: int | None = None
|
|
41
|
+
anis_type: str = "functional"
|
|
42
|
+
axis1: ArrayLike | None = None
|
|
43
|
+
axis2: ArrayLike | None = None
|
|
44
|
+
axis3: ArrayLike | None = None
|
|
45
|
+
K1: EnergyDensity | None = None
|
|
46
|
+
K2: EnergyDensity | None = None
|
|
47
|
+
K3: EnergyDensity | None = None
|
|
48
|
+
stringifier: AnisotropyStringifier | None = field(default=None, repr=False)
|
|
49
|
+
_vectorized_energy: VectorizedEnergyFunction | None = field(
|
|
50
|
+
default=None,
|
|
51
|
+
repr=False,
|
|
52
|
+
compare=False,
|
|
53
|
+
)
|
|
54
|
+
_vectorized_gradient: GradientFunction | None = field(
|
|
55
|
+
default=None,
|
|
56
|
+
repr=False,
|
|
57
|
+
compare=False,
|
|
58
|
+
)
|
|
59
|
+
_vectorized_energy_gradient: EnergyGradientFunction | None = field(
|
|
60
|
+
default=None,
|
|
61
|
+
repr=False,
|
|
62
|
+
compare=False,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
if self.function is None and self.order is None:
|
|
67
|
+
raise ValueError("PredefinedAnisotropy requires either a 'function' or an 'order'.")
|
|
68
|
+
if self.function is not None and not callable(self.function):
|
|
69
|
+
raise TypeError("Anisotropy function must be callable.")
|
|
70
|
+
order = cast(object, self.order)
|
|
71
|
+
if order is not None and (type(order) is bool or not isinstance(order, int) or order <= 0):
|
|
72
|
+
raise ValueError("Anisotropy order must be a positive integer.")
|
|
73
|
+
for name in ("axis1", "axis2", "axis3"):
|
|
74
|
+
axis = getattr(self, name)
|
|
75
|
+
if axis is not None:
|
|
76
|
+
frozen_axis = np.array(axis, dtype=np.float64, copy=True)
|
|
77
|
+
frozen_axis.setflags(write=False)
|
|
78
|
+
object.__setattr__(self, name, frozen_axis)
|
|
79
|
+
|
|
80
|
+
def has_function(self) -> bool:
|
|
81
|
+
return self.function is not None
|
|
82
|
+
|
|
83
|
+
def energy_density(self, magnetisation: ArrayLike) -> FloatArray | float:
|
|
84
|
+
values, scalar = _magnetisation_array(magnetisation)
|
|
85
|
+
if self._vectorized_energy is not None:
|
|
86
|
+
result = np.asarray(self._vectorized_energy(values), dtype=np.float64)
|
|
87
|
+
elif self.function is not None:
|
|
88
|
+
result = _callable_energy(self.function, values)
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError("This anisotropy model has no energy function.")
|
|
91
|
+
if result.shape != (len(values),) or not np.all(np.isfinite(result)):
|
|
92
|
+
raise ValueError("Anisotropy energy evaluation returned invalid values.")
|
|
93
|
+
return float(result[0]) if scalar else result
|
|
94
|
+
|
|
95
|
+
def energy_gradient(self, magnetisation: ArrayLike) -> FloatArray:
|
|
96
|
+
values, scalar = _magnetisation_array(magnetisation)
|
|
97
|
+
if self._vectorized_gradient is not None:
|
|
98
|
+
result = np.asarray(self._vectorized_gradient(values), dtype=np.float64)
|
|
99
|
+
elif self.function is not None:
|
|
100
|
+
result = _finite_difference_gradient(self.function, values)
|
|
101
|
+
else:
|
|
102
|
+
raise ValueError("This anisotropy model has no energy function.")
|
|
103
|
+
if result.shape != values.shape or not np.all(np.isfinite(result)):
|
|
104
|
+
raise ValueError("Anisotropy gradient evaluation returned invalid values.")
|
|
105
|
+
return result[0] if scalar else result
|
|
106
|
+
|
|
107
|
+
def energy_and_gradient(self, magnetisation: ArrayLike) -> tuple[FloatArray, FloatArray]:
|
|
108
|
+
values, _scalar = _magnetisation_array(magnetisation)
|
|
109
|
+
if self._vectorized_energy_gradient is not None:
|
|
110
|
+
energy, gradient = self._vectorized_energy_gradient(values)
|
|
111
|
+
energy = np.asarray(energy, dtype=np.float64)
|
|
112
|
+
gradient = np.asarray(gradient, dtype=np.float64)
|
|
113
|
+
else:
|
|
114
|
+
energy = np.asarray(self.energy_density(values), dtype=np.float64)
|
|
115
|
+
gradient = np.asarray(self.energy_gradient(values), dtype=np.float64)
|
|
116
|
+
if energy.shape != (len(values),) or gradient.shape != values.shape:
|
|
117
|
+
raise ValueError("Anisotropy energy/gradient evaluation returned invalid shapes.")
|
|
118
|
+
if not np.all(np.isfinite(energy)) or not np.all(np.isfinite(gradient)):
|
|
119
|
+
raise ValueError("Anisotropy energy/gradient evaluation returned non-finite values.")
|
|
120
|
+
return energy, gradient
|
|
121
|
+
|
|
122
|
+
def __str__(self) -> str:
|
|
123
|
+
suffix = f", {self.stringifier(self)}" if self.stringifier else ""
|
|
124
|
+
return f"<PredefinedAnisotropy:{self.anis_type}{suffix}>"
|
|
125
|
+
|
|
126
|
+
def __repr__(self) -> str:
|
|
127
|
+
details = self.stringifier(self) if self.stringifier else "?"
|
|
128
|
+
return f'PredefinedAnisotropy(anis_type="{self.anis_type}", {details})'
|
|
129
|
+
|
|
130
|
+
def __neg__(self) -> PredefinedAnisotropy:
|
|
131
|
+
want_anisotropy(self)
|
|
132
|
+
return _combine_anisotropy(self, None, -1.0)
|
|
133
|
+
|
|
134
|
+
def __pos__(self) -> PredefinedAnisotropy:
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def __add__(self, other: object) -> PredefinedAnisotropy:
|
|
138
|
+
want_anisotropy(other)
|
|
139
|
+
assert isinstance(other, PredefinedAnisotropy)
|
|
140
|
+
return _combine_anisotropy(self, other, 1.0)
|
|
141
|
+
|
|
142
|
+
def __sub__(self, other: object) -> PredefinedAnisotropy:
|
|
143
|
+
want_anisotropy(other)
|
|
144
|
+
assert isinstance(other, PredefinedAnisotropy)
|
|
145
|
+
return _combine_anisotropy(self, other, -1.0)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _combine_anisotropy(
|
|
149
|
+
left: PredefinedAnisotropy,
|
|
150
|
+
right: PredefinedAnisotropy | None,
|
|
151
|
+
right_scale: float,
|
|
152
|
+
) -> PredefinedAnisotropy:
|
|
153
|
+
def vectorized_energy_gradient(values: FloatArray) -> tuple[FloatArray, FloatArray]:
|
|
154
|
+
left_energy, left_gradient = left.energy_and_gradient(values)
|
|
155
|
+
if right is None:
|
|
156
|
+
return right_scale * left_energy, right_scale * left_gradient
|
|
157
|
+
right_energy, right_gradient = right.energy_and_gradient(values)
|
|
158
|
+
return (
|
|
159
|
+
left_energy + right_scale * right_energy,
|
|
160
|
+
left_gradient + right_scale * right_gradient,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def vectorized_energy(values: FloatArray) -> FloatArray:
|
|
164
|
+
left_energy = np.asarray(left.energy_density(values), dtype=np.float64)
|
|
165
|
+
if right is None:
|
|
166
|
+
return right_scale * left_energy
|
|
167
|
+
return left_energy + right_scale * np.asarray(
|
|
168
|
+
right.energy_density(values), dtype=np.float64
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def vectorized_gradient(values: FloatArray) -> FloatArray:
|
|
172
|
+
left_gradient = np.asarray(left.energy_gradient(values), dtype=np.float64)
|
|
173
|
+
if right is None:
|
|
174
|
+
return right_scale * left_gradient
|
|
175
|
+
return left_gradient + right_scale * np.asarray(
|
|
176
|
+
right.energy_gradient(values),
|
|
177
|
+
dtype=np.float64,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def scalar_energy(values: ArrayLike) -> float:
|
|
181
|
+
array, _scalar = _magnetisation_array(values)
|
|
182
|
+
return float(vectorized_energy(array)[0])
|
|
183
|
+
|
|
184
|
+
order = left.order if right is None else max(left.order or 0, right.order or 0)
|
|
185
|
+
return PredefinedAnisotropy(
|
|
186
|
+
function=scalar_energy,
|
|
187
|
+
order=order or None,
|
|
188
|
+
_vectorized_energy=vectorized_energy,
|
|
189
|
+
_vectorized_gradient=vectorized_gradient,
|
|
190
|
+
_vectorized_energy_gradient=vectorized_energy_gradient,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def want_anisotropy(value: object, want_function: bool = True) -> None:
|
|
195
|
+
if not isinstance(value, PredefinedAnisotropy):
|
|
196
|
+
raise TypeError(
|
|
197
|
+
f"Operand must be a PredefinedAnisotropy object, not {type(value).__name__}"
|
|
198
|
+
)
|
|
199
|
+
if want_function and not value.has_function():
|
|
200
|
+
raise ValueError("Cannot operate on an anisotropy object that lacks an energy function.")
|
anisotropy/predefined.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Vectorized predefined magnetic anisotropy models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .model import EnergyDensity, PredefinedAnisotropy
|
|
8
|
+
from .values import ArrayLike, FloatArray, _normalize, energy_density_value
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _constant(value: EnergyDensity, name: str) -> float:
|
|
12
|
+
try:
|
|
13
|
+
return energy_density_value(value)
|
|
14
|
+
except (TypeError, ValueError) as exc:
|
|
15
|
+
raise TypeError(f"{name} must be an energy density compatible with J/m^3.") from exc
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def uniaxial_anisotropy(
|
|
19
|
+
axis: ArrayLike,
|
|
20
|
+
K1: EnergyDensity,
|
|
21
|
+
K2: EnergyDensity = 0.0,
|
|
22
|
+
) -> PredefinedAnisotropy:
|
|
23
|
+
"""Create a uniaxial anisotropy energy model.
|
|
24
|
+
|
|
25
|
+
The energy density is ``-K1 (axis·m)^2 - K2 (axis·m)^4``.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
axis: Three-component easy-axis direction; normalized internally.
|
|
29
|
+
K1: Second-order coefficient as J/m³ or a compatible SI quantity.
|
|
30
|
+
K2: Fourth-order coefficient as J/m³ or a compatible SI quantity.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Vectorized predefined anisotropy with analytic gradient evaluation.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If the axis is zero or a coefficient is non-finite.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
normalized_axis = _normalize(axis)
|
|
40
|
+
k1 = _constant(K1, "K1")
|
|
41
|
+
k2 = _constant(K2, "K2")
|
|
42
|
+
|
|
43
|
+
def energy(values: FloatArray) -> FloatArray:
|
|
44
|
+
projection = values @ normalized_axis
|
|
45
|
+
return -k1 * projection**2 - k2 * projection**4
|
|
46
|
+
|
|
47
|
+
def gradient(values: FloatArray) -> FloatArray:
|
|
48
|
+
projection = values @ normalized_axis
|
|
49
|
+
factors = -2.0 * k1 * projection - 4.0 * k2 * projection**3
|
|
50
|
+
return factors[:, np.newaxis] * normalized_axis
|
|
51
|
+
|
|
52
|
+
def energy_gradient(values: FloatArray) -> tuple[FloatArray, FloatArray]:
|
|
53
|
+
projection = values @ normalized_axis
|
|
54
|
+
energy_values = -k1 * projection**2 - k2 * projection**4
|
|
55
|
+
factors = -2.0 * k1 * projection - 4.0 * k2 * projection**3
|
|
56
|
+
return energy_values, factors[:, np.newaxis] * normalized_axis
|
|
57
|
+
|
|
58
|
+
def scalar_energy(values: ArrayLike) -> float:
|
|
59
|
+
vector = np.asarray(values, dtype=np.float64)
|
|
60
|
+
return float(energy(vector[np.newaxis, :])[0])
|
|
61
|
+
|
|
62
|
+
def stringifier(model: PredefinedAnisotropy) -> str:
|
|
63
|
+
assert model.axis1 is not None
|
|
64
|
+
axis_values = np.asarray(model.axis1, dtype=np.float64).tolist()
|
|
65
|
+
result = f"axis={axis_values}, K1={model.K1}"
|
|
66
|
+
if k2 != 0.0:
|
|
67
|
+
result += f", K2={model.K2}"
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
return PredefinedAnisotropy(
|
|
71
|
+
anis_type="uniaxial",
|
|
72
|
+
function=scalar_energy,
|
|
73
|
+
order=4 if k2 != 0.0 else 2,
|
|
74
|
+
axis1=normalized_axis,
|
|
75
|
+
K1=K1,
|
|
76
|
+
K2=K2,
|
|
77
|
+
stringifier=stringifier,
|
|
78
|
+
_vectorized_energy=energy,
|
|
79
|
+
_vectorized_gradient=gradient,
|
|
80
|
+
_vectorized_energy_gradient=energy_gradient,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cubic_anisotropy(
|
|
85
|
+
axis1: ArrayLike,
|
|
86
|
+
axis2: ArrayLike,
|
|
87
|
+
K1: EnergyDensity,
|
|
88
|
+
K2: EnergyDensity = 0.0,
|
|
89
|
+
K3: EnergyDensity = 0.0,
|
|
90
|
+
) -> PredefinedAnisotropy:
|
|
91
|
+
"""Create conventional fourth-, sixth-, and eighth-order cubic anisotropy.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
axis1: First crystalline axis; normalized internally.
|
|
95
|
+
axis2: Second crystalline axis, required to be orthogonal to ``axis1``.
|
|
96
|
+
K1: Fourth-order coefficient as J/m³ or a compatible SI quantity.
|
|
97
|
+
K2: Sixth-order coefficient as J/m³ or a compatible SI quantity.
|
|
98
|
+
K3: Eighth-order coefficient as J/m³ or a compatible SI quantity.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Vectorized predefined anisotropy with analytic gradient evaluation.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If axes are invalid or coefficients are non-finite.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
first = _normalize(axis1)
|
|
108
|
+
raw_second = np.asarray(axis2, dtype=np.float64)
|
|
109
|
+
third = _normalize(np.cross(first, raw_second))
|
|
110
|
+
second = _normalize(np.cross(third, first))
|
|
111
|
+
axes: FloatArray = np.asarray((first, second, third), dtype=np.float64)
|
|
112
|
+
axes_transpose: FloatArray = np.ascontiguousarray(np.transpose(axes))
|
|
113
|
+
k1, k2, k3 = (_constant(value, name) for value, name in ((K1, "K1"), (K2, "K2"), (K3, "K3")))
|
|
114
|
+
|
|
115
|
+
def energy(values: FloatArray) -> FloatArray:
|
|
116
|
+
projections: FloatArray = np.asarray(values @ axes_transpose, dtype=np.float64)
|
|
117
|
+
squared: FloatArray = projections**2
|
|
118
|
+
fourth: FloatArray = squared**2
|
|
119
|
+
pair2 = (
|
|
120
|
+
squared[:, 0] * squared[:, 1]
|
|
121
|
+
+ squared[:, 0] * squared[:, 2]
|
|
122
|
+
+ squared[:, 1] * squared[:, 2]
|
|
123
|
+
)
|
|
124
|
+
triple2 = squared[:, 0] * squared[:, 1] * squared[:, 2]
|
|
125
|
+
pair4 = (
|
|
126
|
+
fourth[:, 0] * fourth[:, 1] + fourth[:, 0] * fourth[:, 2] + fourth[:, 1] * fourth[:, 2]
|
|
127
|
+
)
|
|
128
|
+
return k1 * pair2 + k2 * triple2 + k3 * pair4
|
|
129
|
+
|
|
130
|
+
def gradient(values: FloatArray) -> FloatArray:
|
|
131
|
+
projections: FloatArray = np.asarray(values @ axes_transpose, dtype=np.float64)
|
|
132
|
+
squared: FloatArray = projections**2
|
|
133
|
+
fourth: FloatArray = squared**2
|
|
134
|
+
derivatives: FloatArray = np.empty_like(projections)
|
|
135
|
+
for axis in range(3):
|
|
136
|
+
other = [index for index in range(3) if index != axis]
|
|
137
|
+
derivatives[:, axis] = (
|
|
138
|
+
2.0 * k1 * projections[:, axis] * (squared[:, other[0]] + squared[:, other[1]])
|
|
139
|
+
+ 2.0 * k2 * projections[:, axis] * squared[:, other[0]] * squared[:, other[1]]
|
|
140
|
+
+ 4.0 * k3 * projections[:, axis] ** 3 * (fourth[:, other[0]] + fourth[:, other[1]])
|
|
141
|
+
)
|
|
142
|
+
return derivatives @ axes
|
|
143
|
+
|
|
144
|
+
def energy_gradient(values: FloatArray) -> tuple[FloatArray, FloatArray]:
|
|
145
|
+
projections: FloatArray = np.asarray(values @ axes_transpose, dtype=np.float64)
|
|
146
|
+
squared: FloatArray = projections**2
|
|
147
|
+
fourth: FloatArray = squared**2
|
|
148
|
+
pair2 = (
|
|
149
|
+
squared[:, 0] * squared[:, 1]
|
|
150
|
+
+ squared[:, 0] * squared[:, 2]
|
|
151
|
+
+ squared[:, 1] * squared[:, 2]
|
|
152
|
+
)
|
|
153
|
+
energy_values = (
|
|
154
|
+
k1 * pair2
|
|
155
|
+
+ k2 * squared[:, 0] * squared[:, 1] * squared[:, 2]
|
|
156
|
+
+ k3
|
|
157
|
+
* (
|
|
158
|
+
fourth[:, 0] * fourth[:, 1]
|
|
159
|
+
+ fourth[:, 0] * fourth[:, 2]
|
|
160
|
+
+ fourth[:, 1] * fourth[:, 2]
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
derivatives: FloatArray = np.empty_like(projections)
|
|
164
|
+
for axis in range(3):
|
|
165
|
+
other = [index for index in range(3) if index != axis]
|
|
166
|
+
derivatives[:, axis] = (
|
|
167
|
+
2.0 * k1 * projections[:, axis] * (squared[:, other[0]] + squared[:, other[1]])
|
|
168
|
+
+ 2.0 * k2 * projections[:, axis] * squared[:, other[0]] * squared[:, other[1]]
|
|
169
|
+
+ 4.0 * k3 * projections[:, axis] ** 3 * (fourth[:, other[0]] + fourth[:, other[1]])
|
|
170
|
+
)
|
|
171
|
+
return energy_values, derivatives @ axes
|
|
172
|
+
|
|
173
|
+
def scalar_energy(values: ArrayLike) -> float:
|
|
174
|
+
vector = np.asarray(values, dtype=np.float64)
|
|
175
|
+
return float(energy(vector[np.newaxis, :])[0])
|
|
176
|
+
|
|
177
|
+
def stringifier(model: PredefinedAnisotropy) -> str:
|
|
178
|
+
assert model.axis1 is not None and model.axis2 is not None
|
|
179
|
+
first_values = np.asarray(model.axis1, dtype=np.float64).tolist()
|
|
180
|
+
second_values = np.asarray(model.axis2, dtype=np.float64).tolist()
|
|
181
|
+
result = f"axis1={first_values}, axis2={second_values}, K1={model.K1}"
|
|
182
|
+
if k2 != 0.0:
|
|
183
|
+
result += f", K2={model.K2}"
|
|
184
|
+
if k3 != 0.0:
|
|
185
|
+
result += f", K3={model.K3}"
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
return PredefinedAnisotropy(
|
|
189
|
+
anis_type="cubic",
|
|
190
|
+
function=scalar_energy,
|
|
191
|
+
order=8 if k3 != 0.0 else 6 if k2 != 0.0 else 4,
|
|
192
|
+
axis1=first,
|
|
193
|
+
axis2=second,
|
|
194
|
+
axis3=third,
|
|
195
|
+
K1=K1,
|
|
196
|
+
K2=K2,
|
|
197
|
+
K3=K3,
|
|
198
|
+
stringifier=stringifier,
|
|
199
|
+
_vectorized_energy=energy,
|
|
200
|
+
_vectorized_gradient=gradient,
|
|
201
|
+
_vectorized_energy_gradient=energy_gradient,
|
|
202
|
+
)
|
anisotropy/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker for PEP 561 inline type information.
|
anisotropy/values.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Validation and scalar conversion helpers for anisotropy models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any, TypeAlias, cast
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
|
|
11
|
+
from si.physical import SI
|
|
12
|
+
|
|
13
|
+
FloatArray = NDArray[np.float64]
|
|
14
|
+
# NumPy's ArrayLike alias is intentionally broad, but recent NumPy stubs expose
|
|
15
|
+
# unresolved type variables through it under strict Pyright checking. These
|
|
16
|
+
# inputs are normalized immediately with np.asarray, so Any is intentional at
|
|
17
|
+
# this runtime-validation boundary.
|
|
18
|
+
ArrayLike: TypeAlias = Any
|
|
19
|
+
RawEnergyFunction = Callable[[ArrayLike], object]
|
|
20
|
+
|
|
21
|
+
_ENERGY_DENSITY_UNIT = SI(1.0, "J/m^3")
|
|
22
|
+
_FINITE_DIFFERENCE_STEP = float(np.cbrt(np.finfo(np.float64).eps))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normalize(values: ArrayLike) -> FloatArray:
|
|
26
|
+
vector = np.asarray(values, dtype=np.float64)
|
|
27
|
+
if vector.shape != (3,):
|
|
28
|
+
raise ValueError(f"Anisotropy axes must be 3-vectors, got shape {vector.shape}.")
|
|
29
|
+
if not np.all(np.isfinite(vector)):
|
|
30
|
+
raise ValueError("Anisotropy axes must contain only finite values.")
|
|
31
|
+
norm = float(np.linalg.norm(vector))
|
|
32
|
+
if norm == 0.0:
|
|
33
|
+
raise ValueError("Cannot normalize a zero vector.")
|
|
34
|
+
return vector / norm
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def energy_density_value(value: object) -> float:
|
|
38
|
+
result = (
|
|
39
|
+
value.in_units_of(_ENERGY_DENSITY_UNIT)
|
|
40
|
+
if isinstance(value, SI)
|
|
41
|
+
else float(cast(Any, value))
|
|
42
|
+
)
|
|
43
|
+
if not np.isfinite(result):
|
|
44
|
+
raise ValueError("Anisotropy energy density must be finite.")
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _magnetisation_array(values: ArrayLike) -> tuple[FloatArray, bool]:
|
|
49
|
+
array = np.asarray(values, dtype=np.float64)
|
|
50
|
+
scalar = array.ndim == 1
|
|
51
|
+
if scalar:
|
|
52
|
+
array = array[np.newaxis, :]
|
|
53
|
+
if array.ndim != 2 or array.shape[1] != 3:
|
|
54
|
+
raise ValueError(f"Magnetisation must have shape (3,) or (n, 3), got {array.shape}.")
|
|
55
|
+
if not np.all(np.isfinite(array)):
|
|
56
|
+
raise ValueError("Magnetisation must contain only finite values.")
|
|
57
|
+
return np.ascontiguousarray(array), scalar
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _callable_energy(function: RawEnergyFunction, magnetisation: FloatArray) -> FloatArray:
|
|
61
|
+
return np.asarray(
|
|
62
|
+
[energy_density_value(function(vector)) for vector in magnetisation],
|
|
63
|
+
dtype=np.float64,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _finite_difference_gradient(
|
|
68
|
+
function: RawEnergyFunction,
|
|
69
|
+
magnetisation: FloatArray,
|
|
70
|
+
) -> FloatArray:
|
|
71
|
+
gradient = np.empty_like(magnetisation)
|
|
72
|
+
for row, vector in enumerate(magnetisation):
|
|
73
|
+
for component in range(3):
|
|
74
|
+
step = _FINITE_DIFFERENCE_STEP * max(1.0, abs(float(vector[component])))
|
|
75
|
+
plus = np.array(vector, copy=True)
|
|
76
|
+
minus = np.array(vector, copy=True)
|
|
77
|
+
plus[component] += step
|
|
78
|
+
minus[component] -= step
|
|
79
|
+
gradient[row, component] = (
|
|
80
|
+
energy_density_value(function(plus)) - energy_density_value(function(minus))
|
|
81
|
+
) / (2.0 * step)
|
|
82
|
+
if not np.all(np.isfinite(gradient)):
|
|
83
|
+
raise ValueError("Anisotropy energy gradient must be finite.")
|
|
84
|
+
return gradient
|
mag_material/__init__.py
ADDED