We're working on an end-to-end framework for physically comparable magnetic hysteresis simulations. The framework accepts crystallographic information files (CIF) as input and produces hysteresis loops with quantified key performance indicators (KPIs), while maintaining full provenance tracking. Most importantly, it ensures that simulations run on different engines (micromagnetic and atomistic) use identical material parameters, geometry, microstructure, and protocols—enabling true apples-to-apples comparisons.
This post details the framework's architecture, capabilities, and usage, along with a roadmap for upcoming features including atomistic simulation, advanced microstructure models, and temperature effects.
The framework follows a clean ports-and-adapters architecture that separates physics and planning logic from computational engine specifics. This design enables easy extension to new simulation engines while maintaining consistency in material representation and experimental protocols.
Material Parameter Pipeline: The framework derives all required material parameters from CIF files and first-principles calculations. Given a CIF file and magneto-crystalline anisotropy energy (MAE) from density functional theory (DFT), the system calculates:
Uniaxial anisotropy constant (Ku) from MAE and unit cell volume
Saturation magnetization (Ms) from atomic magnetic moments
Exchange stiffness (A) for micromagnetics
Exchange integrals (J) for atomistic models
Recommended discretization cell sizes based on exchange length
This automatic parameter derivation eliminates manual error and ensures consistency across simulation types.
Geometry Builder: The geometry module creates computational domains that match either:
Supercell dimensions from crystallographic repeats (atomistic)
Specified physical dimensions with appropriate discretization (micromagnetic)
The builder ensures that mesh or lattice discretization respects the exchange length constraint (cell size less than half the exchange length) for numerical stability.
Microstructure Generator: A key innovation is the engine-agnostic microstructure representation. The generator produces:
Voronoi tessellation for polycrystalline structures
Per-grain orientation distributions (Gaussian axis-angle parameterization)
Grain boundary masks with tunable thickness
Defect fields (pores, precipitates) with volume fraction control
The microstructure is represented as callable functions that return spatially-varying properties at any point in space. These functions are then mapped to either continuum fields (micromagnetics) or per-site properties (atomistics), ensuring identical microstructure across engines.
Protocol Engine: Standardized field protocols guarantee that different simulation engines experience identical experimental conditions:
Hysteresis loops: configurable maximum field and step resolution
Field direction control: easy-axis or hard-axis measurements
Relaxation criteria: energy minimization or time-integration tolerances
Multi-cycle protocols for training effects
Data Extraction: Automated analysis extracts standard magnetic performance metrics:
Coercivity (Hc): zero-crossing detection with interpolation
Remanence ratio (Mr/Ms): magnetization at zero field
Energy product (BHmax): maximum in second quadrant
Loop area: integrated hysteresis loss
Provenance Tracking: Every simulation logs complete metadata to MLflow:
Input configurations (YAML serialization)
Material parameters and their derivation
Microstructure random seeds
Engine versions and computational environment
Output artifacts (CSV data, plots, analysis)
This enables full reproducibility and comparison across simulation campaigns.
Start with a CIF file containing the crystal structure of your magnetic material. Provide the MAE per unit cell from DFT calculations or literature... or from a certain MAE model that may or may not be coming soon. For example, Fe₂B with tetragonal structure:
Unit cell: a = 5.109 Å, c = 4.249 Å
MAE: 60 meV/cell (c-axis anisotropy)
Saturation moment: ~2.3 μB per iron atom
The framework automatically extracts the unit cell volume (108.46 ų) and c-axis orientation vector from the CIF file.
Create a YAML configuration file specifying:
engine: "ubermag"
material:
cif_path: "material.cif"
Ms: 1.2e6 # A/m (from experiment or DFT)
MAE_meV_per_cell: 60.0 # meV (from DFT)
A: 8.0e-12 # J/m (micromagnetic exchange)
alpha: 0.05 # Gilbert damping
T: 0.0 # Temperature (K)
easy_axis_from: "c_axis" # Use CIF c-axis
domain:
target_dims_nm: [100, 100, 100] # Physical size
bc: "free" # Boundary conditions
shape: "box"
cell_size_factor: 2.5 # Discretization: lexch/2.5
microstructure:
type: "voronoi" # Grain structure
grains: 12
seed: 42
misorientation:
distribution: "gaussian_axis_angle"
mean_deg: 0.0
std_deg: 20.0 # Texture strength
boundaries:
thickness_nm: 2.0
A_scale: 0.6 # Exchange weakening
Ku_scale: 0.8 # Anisotropy reduction
protocol:
H_max_MA_per_m: 5.0 # Maximum field (MA/m)
steps_per_leg: 31 # Field steps
field_direction: [0, 0, 1] # Along easy axis
relax_mode: "energy_min"
tol: 1.0e-6
logging:
mlflow:
tracking_uri: "file:./mlruns"
experiment: "MaterialStudy"
output_dir: "./artifacts/run_001"
Run the simulation from the command line:
python -m src.cli.run_experiment \
--config configs/my_material.yaml \
--engine ubermag
The framework will:
Parse the CIF and derive Ku = 88.6 MJ/m³
Calculate exchange length: 2.97 nm
Create a mesh with 1.19 nm cells (84×84×84 cells for 100 nm cube)
Generate 12 Voronoi grains with 20° misorientation
Map grain orientations to spatially-varying anisotropy fields
Apply weakened exchange at grain boundaries
Run 153 field steps from +5 to -5 to +5 MA/m
Extract and log KPIs
The simulation produces:
hysteresis_loop.csv
: H and M data points
hysteresis_loop.png
: Plotted M-H curve
kpis.json
: Extracted performance metrics
metadata.json
: Mesh and computational details
Access via MLflow UI for comparison across runs:
mlflow ui --backend-store-uri ./mlruns
The Voronoi tessellation generates realistic polycrystalline microstructures:
Grain Count: Control the number of grains to study size effects. Fewer grains (1-5) model single-domain or oligocrystalline samples. Many grains (50-100) represent bulk polycrystals.
Texture Control: The misorientation standard deviation controls crystallographic texture:
std_deg: 0
→ Perfect single crystal
std_deg: 10-20
→ Strong texture (aligned grains), typical of sintered magnets
std_deg: 30-45
→ Moderate texture
std_deg: 90
→ Random texture (isotropic)
This parameter critically affects coercivity. Aligned textures (10-20°) produce soft behavior with low Hc, while random textures (90°) enable high Hc when sufficient field is applied.
Grain Boundary Engineering:
Adjust exchange coupling and anisotropy at boundaries:
boundaries:
thickness_nm: 2.0 # Physical width
A_scale: 0.6 # 40% reduction in exchange
Ku_scale: 0.8 # 20% reduction in anisotropy
This models boundary phases, segregation, or amorphization that disrupts magnetic coupling between grains.
Pores (Voids):
defects:
pores:
volume_fraction: 0.02 # 2% porosity
size_nm: [3, 8] # Size distribution
Pores act as nucleation sites for magnetic reversal, typically reducing coercivity.
Precipitates (Second Phases):
defects:
precipitates:
volume_fraction: 0.03
phase: "hard" # or "soft"
Ku_scale: 1.5 # 50% higher anisotropy
A_scale: 2.0 # Enhanced exchange
Hard precipitates pin domain walls and increase coercivity. Soft precipitates can create exchange-spring behavior in nanocomposites.
Exchange Energy: For micromagnetics, the framework uses continuum exchange with stiffness A:
E_exchange =
For atomistics (future Phase 1), discrete Heisenberg exchange:
E_exchange =
The mapping A ↔ J is handled consistently: for simple cubic lattices with coordination z and lattice constant a:
Uniaxial Anisotropy: Spatially-varying easy axis and strength:
E_anis =
The framework automatically maps per-grain orientations to smooth field functions for micromagnetics or discrete per-site values for atomistics.
Demagnetization: Full magnetostatic self-interaction via FFT-accelerated Newell method (micromagnetics). For atomistic models, this is handled via dipolar coupling or shape anisotropy approximation.
Zeeman Energy: External field interaction with configurable direction:
E_zeeman =
A critical feature for realistic measurements:
Easy-Axis Loops:
field_direction: [0, 0, 1] # Parallel to easy axis
Measures intrinsic coercivity. High fields required to reverse well-aligned grains.
Hard-Axis Loops:
field_direction: [1, 0, 0] # Perpendicular to easy axis
Measures anisotropy field. Linear response with small magnetization projection.
Arbitrary Directions:
field_direction: [0.707, 0, 0.707] # 45° to easy axis
Studies angular dependence and Stoner-Wohlfarth behavior.
The projected magnetization along the field direction is reported, matching experimental measurement geometry.
The framework interfaces with the Object Oriented MicroMagnetic Framework (OOMMF) via the Ubermag Python API.
Discretization: Finite difference method on a regular rectangular mesh. Cell size automatically set to half the exchange length for stability:
cell_size =
Relaxation: Energy minimization via conjugate gradient method:
driver = MinDriver()
driver.drive(system, tol=1e-6, max_steps=3000)
Each field step relaxes the magnetization to the local energy minimum before recording M.
Performance: A 100×100×100 nm cube (84³ cells) requires approximately:
5-10 seconds per field step in stable regions
30-60 seconds near switching events
Total: 15-30 minutes for a full loop (31 steps per leg)
Future implementation will use Landau-Lifshitz-Gilbert (LLG) integration:
where effective field includes exchange, anisotropy, dipolar, and Zeeman contributions. Relaxation via damping to steady state at each field step.
The configuration system supports:
Base Configurations:
# configs/base.yaml
material:
alpha: 0.05
T: 0.0
domain:
bc: "free"
shape: "box"
Material Libraries:
# configs/materials/fe2b.yaml
Ms: 1.2e6
MAE_meV_per_cell: 60.0
A: 8.0e-12
Experiment Compositions:
# configs/experiments/texture_study.yaml
includes:
- ../base.yaml
- ../materials/fe2b.yaml
microstructure:
misorientation:
std_deg: 20.0
Pydantic-based schema validation ensures:
Required fields present
Correct types and units
Physical constraints (e.g., Ms > 0, Ku ≥ 0)
Mutually exclusive options handled
Invalid configurations are caught immediately with informative error messages.
To demonstrate the framework's predictive capability, we compare engineered versus realistic simulations of Fe₂B.
The realistic_experimental.yaml
configuration represents what would be measured in a vibrating sample magnetometer (VSM) or superconducting quantum interference device (SQUID):
material:
cif_path: "fe2b.cif"
Ms: 1.2e6 # From literature
MAE_meV_per_cell: 60.0 # From DFT
A: 8.0e-12
alpha: 0.05
T: 0.0 # Baseline at 0 K
domain:
target_dims_nm: [100, 100, 100] # Representative volume
microstructure:
type: "voronoi"
grains: 12
misorientation:
std_deg: 20.0 # Realistic sintered texture
protocol:
H_max_MA_per_m: 5.0 # 6.3 Tesla superconducting magnet
field_direction: [0, 0, 1] # Standard easy-axis measurement
Key Performance Indicators:
Coercivity (Hc): 0.0 MA/m
Remanence ratio (Mr/Ms): 0.957
Magnetization range: 1.144 to 1.152 MA/m (always positive)
Interpretation:
The simulation correctly predicts soft magnetic behavior for Fe₂B under realistic processing conditions:
No Coercivity: The 20° texture means most grains are aligned with the applied field direction. The 5 MA/m maximum field is insufficient to reverse these well-aligned grains against their 73 MA/m anisotropy field (Hk = Ku/Ms).
High Remanence: Mr/Ms = 0.96 indicates strong alignment. When field is removed, magnetization remains close to saturation because grains are textured along the measurement direction.
Reversible Rotation: Magnetization undergoes small reversible rotation as field varies but never switches sign. This is characteristic of aligned magnetic materials below their switching threshold.
Physical Validity:
This matches experimental observations of sintered Fe₂B-based materials, which typically show:
Low coercivity: 0.1-2 MA/m (depending on processing)
High remanence in textured samples
Soft magnetic characteristics at practical field strengths
The framework accurately captures that coercivity requires either:
Random grain orientation (opposing easy axes)
Applied fields exceeding the anisotropy field
Thermal activation over energy barriers (T > 0)
Defects providing nucleation sites
None of these conditions are met in the realistic configuration, hence Hc = 0.
For comparison, an engineered configuration with 90° random texture and 150 MA/m field produces:
Coercivity: 54.3 MA/m
Remanence ratio: 0.65
Full magnetization reversal: -1.15 to +1.15 MA/m
This represents the theoretical maximum performance achievable with:
Perfect random texture (impossible to fabricate)
Extreme applied fields (10× practical limits)
Ideal single-phase crystals (no defects)
The framework correctly distinguishes between theoretical limits and practical performance.
Spirit/UppASD Implementation:
Extend the framework to atomistic spin dynamics for direct comparison with micromagnetics:
class SpiritEngine(MagSimEngine):
def setup(self, mat, geom, payload):
# Build spin lattice from CIF supercell
# Assign exchange integrals J_ij from A
# Map microstructure to per-site anisotropy
# Configure LLG integrator
def relax_at_field(self, H_Am):
# Time-integrate LLG to steady state
# Project spin moments along field
# Return average magnetization
Key Capabilities:
Discrete spin representation (S_i on each atom)
Heisenberg exchange with neighbor shells
Per-site anisotropy from grain orientations
Comparison to continuum micromagnetics
Use Cases:
Validate micromagnetic approximation (when does it break?)
Study small systems (<10 nm) where continuum fails
Model site-specific defects (vacancies, dopants)
Capture atomistic details at interfaces
What we want to show: Side-by-side Ubermag vs. Spirit comparison showing coercivity agreement within 20% for representative test cases.
Extended Grain Structures:
Beyond Voronoi tessellation, implement:
Lamellar Structures: Alternating layers for L10-type ordered alloys (FePt, CoPt)
microstructure:
type: "lamellar"
layer_thickness_nm: 5.0
orientation: [0, 0, 1]
Columnar Grains: Thin film growth structures with elongated grains
microstructure:
type: "columnar"
aspect_ratio: 10.0
growth_direction: [0, 0, 1]
Twinned Domains: Crystallographic twins with specific angle relationships
microstructure:
type: "twinned"
twin_type: "cubic_90deg"
Grain Size Distributions: Log-normal or bimodal size distributions
microstructure:
grain_size_distribution:
type: "lognormal"
median_nm: 50.0
sigma: 0.5
Realistic Defects:
Implement defect models that reduce coercivity:
Vacancies: Point defects reducing local magnetic moment
def add_vacancies(sites, concentration=0.01):
# Remove random sites
# Or reduce local Ms by 50%
Dislocations: Line defects creating easy nucleation zones
def add_dislocations(sites, density_m2=1e14):
# Create dislocation cores with Ku_scale=0.3
Stacking Faults: Planar defects interrupting crystal order
Impurity Segregation: Non-magnetic atoms at grain boundaries
Multi-Phase Nanocomposites:
Model exchange-spring magnets with hard-soft phase mixing:
microstructure:
type: "composite"
phases:
hard:
fraction: 0.7
Ku_base: 100e6 # MJ/m³
grain_size_nm: 20
soft:
fraction: 0.3
Ku_base: 1e6
A_scale: 2.0 # Enhanced exchange
grain_size_nm: 5
interface_coupling: "exchange_spring"
Deliverable: Working hard-soft nanocomposite showing enhanced energy product (BHmax) from exchange-spring mechanism.
Temperature-Dependent Properties:
Implement thermal scaling of magnetic parameters:
def apply_temperature_scaling(params, T_K, Tc_K=1000):
"""
Scale properties with reduced magnetization m(T)
following mean-field approximation
"""
m = (1 - (T_K/Tc_K)**2)**0.5 if T_K < Tc_K else 0.0
return {
'Ms': params['Ms'] * m,
'Ku': params['Ku'] * m**2,
'A': params['A'] * m
}
Temperature-dependent simulations enable:
Thermal stability studies (energy barrier height)
Curie temperature determination
Temperature coefficients of Hc and Mr
Thermal demagnetization protocols
Stochastic Thermal Activation:
Add Langevin noise to LLG equation for thermal fluctuations:
+ thermal_noise
This captures:
Thermally-assisted reversal over energy barriers
Viscous damping and loss mechanisms
Time-dependent switching probabilities
Parameter Sweep Framework:
Automated optimization studies:
def parameter_sweep(base_config, sweep_grid):
"""
Example sweep_grid:
{
'material.MAE_meV_per_cell': [40, 60, 80, 100],
'microstructure.misorientation.std_deg': [10, 20, 30, 45, 60],
'domain.target_dims_nm[0]': [50, 100, 200]
}
"""
results = []
for params in grid_iterator(sweep_grid):
config = apply_parameters(base_config, params)
result = run_simulation(config)
results.append({
'params': params,
'Hc': result.kpis['Hc_avg_Am'],
'Mr_Ms': result.kpis['Mr_Ms_ratio'],
'BHmax': result.kpis['BHmax_J_m3']
})
return results
Visualization:
Generate contour plots showing:
Hc vs. texture vs. anisotropy
Energy product vs. grain size vs. composition
Optimal parameter regions highlighted
Dynamic Hysteresis:
Frequency-dependent measurements:
protocol:
mode: "dynamic"
frequency_Hz: [0.01, 0.1, 1, 10, 100]
H_amplitude_MA_per_m: 5.0
Studies:
AC loss mechanisms
Frequency-dependent coercivity
Minor loop behavior
First-order reversal curves (FORC)
What we want to show: Automated optimization framework producing contour maps of performance metrics across multi-dimensional parameter spaces.
The framework's engine-agnostic design enables straightforward integration of new solvers. Implement the MagSimEngine
protocol:
class CustomEngine:
def setup(self, mat: MaterialParams,
geom: GeometrySpec,
payload: Dict[str, Any]) -> None:
"""
Initialize solver with material, geometry, and microstructure
Args:
mat: MaterialParams with Ms, Ku, A, alpha, etc.
geom: GeometrySpec with dimensions and discretization
payload: Microstructure fields or per-site data
"""
# Setup custom solver
def relax_at_field(self, H_Am: float) -> Dict[str, Any]:
"""
Relax system at applied field and return magnetization
Args:
H_Am: Applied field magnitude (A/m) along field_direction
Returns:
{'H_Am': H_Am, 'M_Am': M_projected, ...}
"""
# Run relaxation
# Project magnetization along field_direction
return results
def finalize(self) -> SimulationResult:
"""Return complete H and M arrays"""
return SimulationResult(H_Am=self.H_list, M_Am=self.M_list)
Register the new engine:
# src/cli/run_experiment.py
def run_custom_experiment(config):
engine = CustomEngine()
engine.setup(mat_params, geom_spec, payload)
# ... standard experiment loop
The microstructure payload automatically adapts to the engine's needs via the assignment layer.
Extend the microstructure generator:
class CustomMicrostructure:
def generate(self, dims_m):
"""
Return functions mapping position to properties
Returns:
{
'easy_axis': lambda pos: unit_vector,
'scale_Ku': lambda pos: scale_factor,
'scale_A': lambda pos: scale_factor,
'scale_Ms': lambda pos: scale_factor,
'grain_label': lambda pos: int,
'is_boundary': lambda pos: bool,
'is_pore': lambda pos: bool
}
"""
# Custom microstructure logic
return field_functions
Register in the factory:
# src/core/microstructure.py
MICROSTRUCTURE_TYPES = {
'voronoi': VoronoiMicrostructure,
'lamellar': LamellarMicrostructure,
'custom': CustomMicrostructure
}
Typical simulation times on a workstation (8-core, 32 GB RAM):
Micromagnetics (Ubermag/OOMMF):
50×50×50 nm (42³ cells): 5 min for 31-step loop
100×100×100 nm (84³ cells): 20 min for 31-step loop
200×200×200 nm (168³ cells): 2 hours for 31-step loop
Scales approximately as N_cells^1.3 due to FFT-based demagnetization.
Atomistics (future):
10×10×10 nm (10⁴ spins): 30 min estimated
20×20×20 nm (10⁵ spins): 5 hours estimated
Scales approximately as N_spins × N_neighbors × N_steps.
Adaptive Stepping: Use fine steps near coercivity, coarse steps in saturated regions:
def adaptive_field_schedule(H_max, Hc_estimate):
# Dense sampling near ±Hc
# Sparse sampling far from reversal
Warm Starting: Initialize each field step from the previous magnetization state rather than random or saturated. Reduces convergence iterations.
Parallel Execution: Parameter sweeps are embarrassingly parallel:
from multiprocessing import Pool
with Pool(8) as p:
results = p.map(run_simulation, config_list)
Reduced Symmetry: For single-grain or highly symmetric cases, simulate only a representative volume element.
Automated validation ensures physical consistency:
Energy Conservation: Check that relaxed state is at local energy minimum
Coercivity Bounds: Verify Hc ≤ Hk (anisotropy field) Verify Hc ≤ H_max (can't measure higher than applied field)
Magnetization Magnitude: Check |M| ≤ Ms at all times
Remanence Bounds: Verify 0 ≤ Mr/Ms ≤ 1
Loop Closure: Final state should match initial state (within tolerance)
Golden test suite runs on every code change:
def test_fe2b_single_grain():
"""Baseline test with known expected values"""
config = load_config('tests/golden/fe2b_single.yaml')
result = run_simulation(config)
assert abs(result.kpis['Hc_avg_Am'] - 0.0) < 1e3 # Hc near zero
assert abs(result.kpis['Mr_Ms_ratio'] - 1.0) < 0.01 # Near saturation
Verify mesh independence:
def test_mesh_convergence():
"""Test Hc convergence with refinement"""
cell_sizes = [5.0, 2.5, 1.25, 0.625] # nm
Hc_values = [run_simulation(cell_size).kpis['Hc']
for cell_size in cell_sizes]
# Hc should converge (relative change < 5%)
assert (Hc_values[-1] - Hc_values[-2]) / Hc_values[-2] < 0.05
The framework facilitates direct comparison with measurements:
Import VSM/SQUID Data:
def import_vsm_data(filename):
"""Load experimental M-H curve"""
H_exp, M_exp = load_vsm(filename)
# Convert units if needed (Oe → A/m, emu/g → A/m)
H_exp_Am = H_exp * 1e3 / (4*pi) # Oe to A/m
return H_exp_Am, M_exp
Overlay Simulation and Experiment:
def compare_to_experiment(sim_result, exp_data):
"""Generate comparison plot"""
fig, ax = plt.subplots()
ax.plot(sim_result.H_Am, sim_result.M_Am,
label='Simulation', linewidth=2)
ax.plot(exp_data.H_Am, exp_data.M_Am,
label='Experiment', linestyle='--', linewidth=2)
ax.set_xlabel('Applied Field (MA/m)')
ax.set_ylabel('Magnetization (MA/m)')
ax.legend()
return fig
Parameter Fitting:
Optimize simulation parameters to match experimental loop:
from scipy.optimize import minimize
def objective(params):
"""Minimize difference between sim and exp"""
config.material.Ms = params[0]
config.material.MAE_meV_per_cell = params[1]
config.microstructure.misorientation.std_deg = params[2]
sim_result = run_simulation(config)
error = np.sum((sim_result.M_Am - exp_M_Am)**2)
return error
# Find best-fit parameters
result = minimize(objective, x0=[1.2e6, 60.0, 20.0])
This entire build will continue to evolve... more to come as always.