quantized-lab 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
r"""Magnetic-properties calculators (DiraCulator ``buildMagneticTab`` +
|
|
2
|
+
``+calc/+magnetic``).
|
|
3
|
+
|
|
4
|
+
Pure calc layer — closed-form scalars in, result dicts out. No fastapi /
|
|
5
|
+
pydantic imports. Ports the MATLAB magnetic-tab cards and their backing
|
|
6
|
+
``calc.magnetic`` functions:
|
|
7
|
+
|
|
8
|
+
.. math::
|
|
9
|
+
|
|
10
|
+
m\,[\mu_B] = m / \mu_{B,\text{cgs}} \qquad
|
|
11
|
+
M_\text{cgs} = m / V \qquad M_\text{SI} = 1000\,M_\text{cgs} \\
|
|
12
|
+
N_z + 2 N_{xy} = 1 \qquad
|
|
13
|
+
\mu_\text{eff} = \sqrt{3 k_B C / (N_A \mu_B^2)}\ [\mu_B] \\
|
|
14
|
+
\chi = C/(T-\theta) \qquad
|
|
15
|
+
L(x) = \coth x - 1/x,\ \ x = \mu H/(k_B T) \\
|
|
16
|
+
\delta = \pi\sqrt{A/K} \qquad E_\text{wall} = 4\sqrt{A K}
|
|
17
|
+
|
|
18
|
+
This is the **magnetic CALCULATOR** tab (unit conversions, demagnetizing
|
|
19
|
+
factors, Curie–Weiss, Langevin, domain walls) — distinct from
|
|
20
|
+
``calc.magnetometry`` (hysteresis-loop analysis).
|
|
21
|
+
|
|
22
|
+
Unit convention follows the MATLAB toolbox (CGS for instrument data): moment in
|
|
23
|
+
emu, magnetization in emu/cm³ (= 1000 A/m), field in Oe, exchange stiffness A in
|
|
24
|
+
erg/cm, anisotropy K in erg/cm³. CGS constants are derived from the SI
|
|
25
|
+
``constants()`` (no retyping): ``muB_cgs = muB_SI·1e3`` emu, ``kB_cgs =
|
|
26
|
+
kB_SI·1e7`` erg/K.
|
|
27
|
+
|
|
28
|
+
Reference values (closed form / MATLAB docstrings):
|
|
29
|
+
- ``bohr_magneton_convert(9.2740100783e-21, 'emu') -> mu_b = 1.0``
|
|
30
|
+
- ``magnetization(2.5e-3, 5e-5) -> M_si = 50000`` A/m
|
|
31
|
+
- ``demag_factor('sphere') -> Nz = 1/3``; ``'thin_film' -> Nz = 1``
|
|
32
|
+
- ``curie_weiss_moment(4.375, -50) -> mu_eff ≈ 5.91`` µ_B (≈ 2.828·√C)
|
|
33
|
+
- ``domain_wall(2e-6, 4.8e6) -> delta ≈ 20.3`` nm
|
|
34
|
+
|
|
35
|
+
MATLAB source bugs found and corrected here (see module-level notes):
|
|
36
|
+
1. ``DiraCulator.doCurieWeiss`` / ``curieWeiss.m`` µ_eff: the GUI card is
|
|
37
|
+
correct (≈ 2.828·√C); ``curieWeiss.m`` uses ``C_SI = C*1e-3`` with SI
|
|
38
|
+
constants, making its ``mu_eff`` ~100× too small. We freeze the
|
|
39
|
+
physically-correct CGS form (matching the GUI and textbook).
|
|
40
|
+
2. ``DiraCulator.doDomainWall`` wall-energy conversion: it multiplies by
|
|
41
|
+
``1e-3*1e4 = 10`` to go erg/cm² → mJ/m², but 1 erg/cm² = 1 mJ/m² exactly.
|
|
42
|
+
We use the correct ×1 factor (Co ≈ 15 mJ/m²).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import math
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
import numpy as np
|
|
51
|
+
|
|
52
|
+
from quantized.calc.constants import constants
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"bohr_magneton_convert",
|
|
56
|
+
"curie_weiss_fit",
|
|
57
|
+
"curie_weiss_moment",
|
|
58
|
+
"demag_factor",
|
|
59
|
+
"demag_named",
|
|
60
|
+
"domain_wall",
|
|
61
|
+
"langevin",
|
|
62
|
+
"magnetization",
|
|
63
|
+
"moment_convert",
|
|
64
|
+
"moment_per_atom",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# CGS constants derived from the SI CODATA values (no retyping).
|
|
68
|
+
# 1 emu = 1e-3 A*m^2 = 1e-3 J/T -> muB_cgs = muB_SI * 1e3 emu
|
|
69
|
+
# 1 J = 1e7 erg -> kB_cgs = kB_SI * 1e7 erg/K
|
|
70
|
+
_MUB_SI = constants()["muB"] # J/T = A*m^2
|
|
71
|
+
_MUB_CGS = _MUB_SI * 1e3 # emu (9.2740100783e-21)
|
|
72
|
+
_KB_CGS = constants()["kB"] * 1e7 # erg/K (1.380649e-16)
|
|
73
|
+
_NA = constants()["NA"] # 1/mol
|
|
74
|
+
|
|
75
|
+
# Input-unit -> emu scale factor (DiraCulator moment dropdown ItemsData).
|
|
76
|
+
_MOMENT_UNIT_SCALE: dict[str, float] = {
|
|
77
|
+
"emu": 1.0,
|
|
78
|
+
"Am2": 1e3, # 1 A*m^2 = 1e3 emu
|
|
79
|
+
"memu": 1e-3,
|
|
80
|
+
"uemu": 1e-6,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def bohr_magneton_convert(moment: float, unit: str) -> dict[str, Any]:
|
|
85
|
+
"""Convert a magnetic moment to a number of Bohr magnetons (``bohrMagnetonConvert.m``).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
moment: magnetic-moment value.
|
|
89
|
+
unit: ``'emu'`` (CGS, 1 emu = 1e-3 A·m²), ``'Am2'`` or ``'JT'`` (SI,
|
|
90
|
+
J/T = A·m²).
|
|
91
|
+
|
|
92
|
+
>>> round(bohr_magneton_convert(9.2740100783e-21, "emu")["mu_b"], 9)
|
|
93
|
+
1.0
|
|
94
|
+
>>> round(bohr_magneton_convert(9.2740100783e-24, "Am2")["mu_b"], 9)
|
|
95
|
+
1.0
|
|
96
|
+
"""
|
|
97
|
+
if unit == "emu":
|
|
98
|
+
mu_b = moment / _MUB_CGS
|
|
99
|
+
elif unit in ("Am2", "JT"):
|
|
100
|
+
mu_b = moment / _MUB_SI
|
|
101
|
+
else:
|
|
102
|
+
raise ValueError("unit must be 'emu', 'Am2', or 'JT'")
|
|
103
|
+
return {"mu_b": mu_b, "moment": moment, "unit": unit}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def magnetization(moment: float, volume: float) -> dict[str, float]:
|
|
107
|
+
"""Magnetization from moment and volume (``magnetization.m``).
|
|
108
|
+
|
|
109
|
+
``M_cgs = m/V`` (emu/cm³); ``M_SI = 1000·M_cgs`` (A/m), since
|
|
110
|
+
1 emu/cm³ = 1000 A/m.
|
|
111
|
+
|
|
112
|
+
>>> round(magnetization(2.5e-3, 5e-5)["m_si"], 6)
|
|
113
|
+
50000.0
|
|
114
|
+
"""
|
|
115
|
+
if volume <= 0:
|
|
116
|
+
raise ValueError("volume must be positive")
|
|
117
|
+
m_cgs = moment / volume
|
|
118
|
+
m_si = m_cgs * 1000.0
|
|
119
|
+
return {"m_cgs": m_cgs, "m_si": m_si, "m_kam": m_si / 1000.0}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def moment_per_atom(
|
|
123
|
+
total_moment: float, volume: float, atom_density: float
|
|
124
|
+
) -> dict[str, float]:
|
|
125
|
+
"""Per-atom moment in Bohr magnetons (``momentPerAtom.m``).
|
|
126
|
+
|
|
127
|
+
``M = m/V`` (emu/cm³); ``mu_emu = M/n`` (emu/atom); ``mu_b = mu_emu/μ_B``.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
total_moment: total sample moment (emu).
|
|
131
|
+
volume: sample volume (cm³), > 0.
|
|
132
|
+
atom_density: atomic number density (atoms/cm³), > 0.
|
|
133
|
+
"""
|
|
134
|
+
if volume <= 0 or atom_density <= 0:
|
|
135
|
+
raise ValueError("volume and atom_density must be positive")
|
|
136
|
+
m_cgs = total_moment / volume
|
|
137
|
+
mu_emu = m_cgs / atom_density
|
|
138
|
+
return {"mu_b": mu_emu / _MUB_CGS, "mu_emu": mu_emu, "m_cgs": m_cgs}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def moment_convert(
|
|
142
|
+
value: float,
|
|
143
|
+
unit: str = "emu",
|
|
144
|
+
*,
|
|
145
|
+
volume: float | None = None,
|
|
146
|
+
atoms: float | None = None,
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""Moment-conversion card (``DiraCulator.doMomentConvert``).
|
|
149
|
+
|
|
150
|
+
Converts a moment in ``emu`` / ``Am2`` / ``memu`` / ``uemu`` to emu and SI
|
|
151
|
+
(A·m²), and to total Bohr magnetons. Optionally returns magnetization
|
|
152
|
+
(if ``volume`` > 0, cm³) and µ_B/atom (if ``atoms`` > 0, a count).
|
|
153
|
+
|
|
154
|
+
>>> r = moment_convert(1.0e-3, "emu", volume=0.01)
|
|
155
|
+
>>> round(r["am2"], 9)
|
|
156
|
+
1e-06
|
|
157
|
+
"""
|
|
158
|
+
if unit not in _MOMENT_UNIT_SCALE:
|
|
159
|
+
raise ValueError("unit must be one of emu, Am2, memu, uemu")
|
|
160
|
+
emu = value * _MOMENT_UNIT_SCALE[unit]
|
|
161
|
+
am2 = emu * 1e-3
|
|
162
|
+
mu_b = emu / _MUB_CGS
|
|
163
|
+
m_cgs: float | None = None
|
|
164
|
+
m_si: float | None = None
|
|
165
|
+
mu_b_per_atom: float | None = None
|
|
166
|
+
if volume is not None and volume > 0:
|
|
167
|
+
m_cgs = emu / volume
|
|
168
|
+
m_si = m_cgs * 1e3
|
|
169
|
+
if atoms is not None and atoms > 0:
|
|
170
|
+
mu_b_per_atom = mu_b / atoms
|
|
171
|
+
return {
|
|
172
|
+
"emu": emu,
|
|
173
|
+
"am2": am2,
|
|
174
|
+
"mu_b": mu_b,
|
|
175
|
+
"m_cgs": m_cgs,
|
|
176
|
+
"m_si": m_si,
|
|
177
|
+
"mu_b_per_atom": mu_b_per_atom,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ── Demagnetizing factors ───────────────────────────────────────────────────
|
|
182
|
+
_DEMAG_SHAPES = ("sphere", "thin_film", "cylinder", "prolate", "oblate")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def demag_factor(
|
|
186
|
+
shape: str,
|
|
187
|
+
*,
|
|
188
|
+
length: float = 1.0,
|
|
189
|
+
diameter: float = 1.0,
|
|
190
|
+
ratio: float = 2.0,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
"""Demagnetizing factors for common geometries (``demagFactor.m``).
|
|
193
|
+
|
|
194
|
+
Returns ``Nz`` along the symmetry axis and ``Nxy = (1 - Nz)/2`` (so
|
|
195
|
+
``Nz + 2·Nxy = 1``, SI convention; CGS uses ``4π·N``).
|
|
196
|
+
|
|
197
|
+
Shapes: ``'sphere'`` (Nz=1/3), ``'thin_film'`` (Nz=1, infinite slab),
|
|
198
|
+
``'cylinder'`` (Sato–Ishii ``1/(1+1.6·L/d)``, valid L/d∈[0.1,10]),
|
|
199
|
+
``'prolate'`` (rod, ratio=c/a>1), ``'oblate'`` (disk, ratio=a/c>1) —
|
|
200
|
+
both spheroids via the exact Osborn (1945) formulas.
|
|
201
|
+
|
|
202
|
+
>>> round(demag_factor("sphere")["Nz"], 6)
|
|
203
|
+
0.333333
|
|
204
|
+
>>> demag_factor("thin_film")["Nz"]
|
|
205
|
+
1.0
|
|
206
|
+
"""
|
|
207
|
+
if shape not in _DEMAG_SHAPES:
|
|
208
|
+
raise ValueError(f"shape must be one of {_DEMAG_SHAPES}")
|
|
209
|
+
|
|
210
|
+
if shape == "sphere":
|
|
211
|
+
nz = 1.0 / 3.0
|
|
212
|
+
elif shape == "thin_film":
|
|
213
|
+
nz = 1.0
|
|
214
|
+
elif shape == "cylinder":
|
|
215
|
+
if length <= 0 or diameter <= 0:
|
|
216
|
+
raise ValueError("length and diameter must be positive")
|
|
217
|
+
nz = 1.0 / (1.0 + 1.6 * (length / diameter))
|
|
218
|
+
elif shape == "prolate":
|
|
219
|
+
if ratio <= 1:
|
|
220
|
+
raise ValueError("prolate requires ratio = c/a > 1")
|
|
221
|
+
e2 = 1.0 - (1.0 / ratio) ** 2
|
|
222
|
+
e = math.sqrt(e2)
|
|
223
|
+
nz = (1.0 - e2) / e2 * (-1.0 + 1.0 / (2.0 * e) * math.log((1.0 + e) / (1.0 - e)))
|
|
224
|
+
else: # oblate
|
|
225
|
+
if ratio <= 1:
|
|
226
|
+
raise ValueError("oblate requires ratio = a/c > 1")
|
|
227
|
+
e2 = 1.0 - (1.0 / ratio) ** 2
|
|
228
|
+
e = math.sqrt(e2)
|
|
229
|
+
nz = (1.0 / e2) * (1.0 - math.sqrt(1.0 - e2) / e * math.asin(e))
|
|
230
|
+
|
|
231
|
+
nxy = (1.0 - nz) / 2.0
|
|
232
|
+
return {"Nz": nz, "Nxy": nxy, "shape": shape, "n_cgs": 4.0 * math.pi * nz}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# DiraCulator demag dropdown labels -> canonical-shape calls (with axis swap).
|
|
236
|
+
_DEMAG_NAMED = (
|
|
237
|
+
"Sphere",
|
|
238
|
+
"Thin film (in-plane)",
|
|
239
|
+
"Thin film (out-of-plane)",
|
|
240
|
+
"Long cylinder (axial)",
|
|
241
|
+
"Long cylinder (transverse)",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def demag_named(label: str) -> dict[str, Any]:
|
|
246
|
+
"""Demag factor by GUI dropdown label (``DiraCulator.doDemagFactor``).
|
|
247
|
+
|
|
248
|
+
Maps the human-readable geometry to ``demag_factor`` and applies the GUI's
|
|
249
|
+
axis swaps: in-plane film and transverse cylinder report ``Nxy``↔``Nz``
|
|
250
|
+
swapped (long rods use a prolate spheroid with aspect ratio 20).
|
|
251
|
+
|
|
252
|
+
>>> demag_named("Sphere")["Nz"]
|
|
253
|
+
0.3333333333333333
|
|
254
|
+
"""
|
|
255
|
+
if label == "Sphere":
|
|
256
|
+
r = demag_factor("sphere")
|
|
257
|
+
elif label == "Thin film (out-of-plane)":
|
|
258
|
+
r = demag_factor("thin_film")
|
|
259
|
+
elif label == "Thin film (in-plane)":
|
|
260
|
+
r = demag_factor("thin_film")
|
|
261
|
+
r["Nz"], r["Nxy"] = r["Nxy"], r["Nz"]
|
|
262
|
+
elif label == "Long cylinder (axial)":
|
|
263
|
+
r = demag_factor("prolate", ratio=20)
|
|
264
|
+
elif label == "Long cylinder (transverse)":
|
|
265
|
+
r = demag_factor("prolate", ratio=20)
|
|
266
|
+
r["Nz"], r["Nxy"] = r["Nxy"], r["Nz"]
|
|
267
|
+
else:
|
|
268
|
+
raise ValueError(f"shape must be one of {_DEMAG_NAMED}")
|
|
269
|
+
r["shape"] = label
|
|
270
|
+
r["n_cgs"] = 4.0 * math.pi * r["Nz"]
|
|
271
|
+
return r
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ── Curie–Weiss law ─────────────────────────────────────────────────────────
|
|
275
|
+
def _mag_type(theta: float) -> str:
|
|
276
|
+
if theta < 0:
|
|
277
|
+
return "antiferromagnetic"
|
|
278
|
+
if theta > 0:
|
|
279
|
+
return "ferromagnetic"
|
|
280
|
+
return "paramagnetic"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _mu_eff_from_C(c: float) -> float:
|
|
284
|
+
"""Effective moment (µ_B) from the molar CGS Curie constant C (emu·K/mol).
|
|
285
|
+
|
|
286
|
+
``mu_eff = sqrt(3·kB·C / (NA·muB²))`` with CGS units ≈ 2.828·√C. This is the
|
|
287
|
+
physically-correct form (matches the GUI card and the textbook
|
|
288
|
+
``p_eff = 2.828·√(χT)``); ``curieWeiss.m`` is ~100× low — see module notes.
|
|
289
|
+
"""
|
|
290
|
+
return math.sqrt(max(3.0 * _KB_CGS * c / (_NA * _MUB_CGS**2), 0.0))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def curie_weiss_moment(c: float, theta: float) -> dict[str, Any]:
|
|
294
|
+
"""Curie–Weiss card (``DiraCulator.doCurieWeiss``).
|
|
295
|
+
|
|
296
|
+
From the Curie constant C (emu·K/mol) and Weiss temperature θ (K), returns
|
|
297
|
+
the effective moment (µ_B) and the magnetic-order type from sign(θ).
|
|
298
|
+
|
|
299
|
+
>>> round(curie_weiss_moment(4.375, -50)["mu_eff"], 2)
|
|
300
|
+
5.91
|
|
301
|
+
>>> curie_weiss_moment(4.375, -50)["mag_type"]
|
|
302
|
+
'antiferromagnetic'
|
|
303
|
+
"""
|
|
304
|
+
if c < 0:
|
|
305
|
+
raise ValueError("Curie constant C must be non-negative")
|
|
306
|
+
return {"mu_eff": _mu_eff_from_C(c), "C": c, "theta": theta, "mag_type": _mag_type(theta)}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def curie_weiss_fit(
|
|
310
|
+
temperature: list[float],
|
|
311
|
+
susceptibility: list[float],
|
|
312
|
+
*,
|
|
313
|
+
fit_range: tuple[float, float] | None = None,
|
|
314
|
+
) -> dict[str, Any]:
|
|
315
|
+
"""Curie–Weiss parameters from a χ(T) sweep via a 1/χ vs T fit (``curieWeiss.m``).
|
|
316
|
+
|
|
317
|
+
Fits ``1/χ = T/C - θ/C`` (linear); slope → C, intercept → θ. Non-positive χ
|
|
318
|
+
points are excluded. When ``fit_range`` is None the fit uses points at and
|
|
319
|
+
above the temperature where 1/χ is maximal (the paramagnetic regime).
|
|
320
|
+
|
|
321
|
+
Returns ``theta_cw`` (K), ``C`` (emu·K/mol), ``mu_eff`` (µ_B, corrected CGS
|
|
322
|
+
form), ``fit_line`` ``[slope, intercept]``, ``r2`` and ``inv_chi``.
|
|
323
|
+
"""
|
|
324
|
+
t = np.asarray(temperature, dtype=float)
|
|
325
|
+
chi = np.asarray(susceptibility, dtype=float)
|
|
326
|
+
if t.size < 3:
|
|
327
|
+
raise ValueError("need at least 3 data points")
|
|
328
|
+
if chi.size != t.size:
|
|
329
|
+
raise ValueError("temperature and susceptibility must be the same length")
|
|
330
|
+
|
|
331
|
+
valid = chi > 0
|
|
332
|
+
inv_chi = np.full(t.size, np.nan, dtype=float)
|
|
333
|
+
inv_chi[valid] = 1.0 / chi[valid]
|
|
334
|
+
t_v = t[valid]
|
|
335
|
+
ic_v = inv_chi[valid]
|
|
336
|
+
if t_v.size == 0:
|
|
337
|
+
raise ValueError("all susceptibility values are non-positive")
|
|
338
|
+
|
|
339
|
+
if fit_range is None:
|
|
340
|
+
i_max = int(np.argmax(ic_v))
|
|
341
|
+
mask = t_v >= t_v[i_max]
|
|
342
|
+
else:
|
|
343
|
+
lo, hi = fit_range
|
|
344
|
+
mask = (t_v >= lo) & (t_v <= hi)
|
|
345
|
+
if int(np.count_nonzero(mask)) < 2:
|
|
346
|
+
mask = np.ones(t_v.size, dtype=bool)
|
|
347
|
+
|
|
348
|
+
t_fit = t_v[mask]
|
|
349
|
+
ic_fit = ic_v[mask]
|
|
350
|
+
amat = np.vstack([t_fit, np.ones(t_fit.size)]).T
|
|
351
|
+
coef = np.asarray(np.linalg.lstsq(amat, ic_fit, rcond=None)[0], dtype=float)
|
|
352
|
+
slope = float(coef[0])
|
|
353
|
+
intercept = float(coef[1])
|
|
354
|
+
if abs(slope) < np.finfo(float).eps:
|
|
355
|
+
raise ValueError("fitted slope is essentially zero; data may not be Curie-Weiss")
|
|
356
|
+
|
|
357
|
+
c = 1.0 / slope
|
|
358
|
+
theta_cw = -intercept / slope
|
|
359
|
+
pred = slope * t_fit + intercept
|
|
360
|
+
ss_tot = float(np.sum((ic_fit - ic_fit.mean()) ** 2))
|
|
361
|
+
ss_res = float(np.sum((ic_fit - pred) ** 2))
|
|
362
|
+
r2 = 1.0 - ss_res / max(ss_tot, np.finfo(float).eps)
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
"theta_cw": theta_cw,
|
|
366
|
+
"C": c,
|
|
367
|
+
"mu_eff": _mu_eff_from_C(c) if c > 0 else float("nan"),
|
|
368
|
+
"fit_line": [slope, intercept],
|
|
369
|
+
"r2": r2,
|
|
370
|
+
"inv_chi": inv_chi.tolist(),
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ── Langevin / superparamagnetism ───────────────────────────────────────────
|
|
375
|
+
def langevin(mu: float, field_oe: float, temperature: float) -> dict[str, Any]:
|
|
376
|
+
"""Langevin function for a superparamagnet (``DiraCulator.doLangevin``).
|
|
377
|
+
|
|
378
|
+
``x = μ·H/(k_B·T)`` (CGS: μ in emu, H in Oe, T in K); ``L(x) = coth x − 1/x``
|
|
379
|
+
(→ 0 as x → 0). Also returns the moment in Bohr magnetons.
|
|
380
|
+
|
|
381
|
+
>>> round(langevin(1e-16, 10000.0, 300.0)["L"], 6) >= 0.0
|
|
382
|
+
True
|
|
383
|
+
"""
|
|
384
|
+
if mu < 0:
|
|
385
|
+
raise ValueError("moment mu must be non-negative")
|
|
386
|
+
if temperature <= 0:
|
|
387
|
+
raise ValueError("temperature must be > 0 K")
|
|
388
|
+
x = mu * field_oe / (_KB_CGS * temperature)
|
|
389
|
+
lval = 0.0 if abs(x) < 1e-10 else 1.0 / math.tanh(x) - 1.0 / x
|
|
390
|
+
return {"L": lval, "x": x, "n_mu_b": mu / _MUB_CGS}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ── Domain wall & anisotropy ────────────────────────────────────────────────
|
|
394
|
+
def domain_wall(exchange_a: float, anisotropy_k: float) -> dict[str, Any]:
|
|
395
|
+
"""Bloch domain-wall width and energy (``DiraCulator.doDomainWall``).
|
|
396
|
+
|
|
397
|
+
``δ = π·√(A/K)`` (cm) and ``E_wall = 4·√(A·K)`` (erg/cm²), with A the
|
|
398
|
+
exchange stiffness (erg/cm) and K the uniaxial anisotropy (erg/cm³). Width is
|
|
399
|
+
returned in nm and energy in mJ/m² (1 erg/cm² = 1 mJ/m² exactly — the GUI's
|
|
400
|
+
×10 conversion is a bug; see module notes).
|
|
401
|
+
|
|
402
|
+
>>> round(domain_wall(2e-6, 4.8e6)["delta_nm"], 1)
|
|
403
|
+
20.3
|
|
404
|
+
"""
|
|
405
|
+
if exchange_a <= 0 or anisotropy_k <= 0:
|
|
406
|
+
raise ValueError("A and K must be positive")
|
|
407
|
+
delta_cm = math.pi * math.sqrt(exchange_a / anisotropy_k)
|
|
408
|
+
e_wall_erg = 4.0 * math.sqrt(exchange_a * anisotropy_k)
|
|
409
|
+
return {
|
|
410
|
+
"delta_cm": delta_cm,
|
|
411
|
+
"delta_nm": delta_cm * 1e7,
|
|
412
|
+
"e_wall_erg_cm2": e_wall_erg,
|
|
413
|
+
"e_wall_mj_m2": e_wall_erg, # 1 erg/cm^2 = 1 mJ/m^2
|
|
414
|
+
}
|