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.
Files changed (233) hide show
  1. quantized/__init__.py +11 -0
  2. quantized/__main__.py +14 -0
  3. quantized/api.py +220 -0
  4. quantized/app.py +206 -0
  5. quantized/calc/__init__.py +8 -0
  6. quantized/calc/_clipfit.py +76 -0
  7. quantized/calc/_natural_neighbor.py +219 -0
  8. quantized/calc/aggregate.py +159 -0
  9. quantized/calc/backgrounds.py +353 -0
  10. quantized/calc/baseline.py +349 -0
  11. quantized/calc/batch_fit.py +148 -0
  12. quantized/calc/constants.py +27 -0
  13. quantized/calc/corrections.py +192 -0
  14. quantized/calc/crystallography.py +400 -0
  15. quantized/calc/diffusion.py +120 -0
  16. quantized/calc/electrical.py +248 -0
  17. quantized/calc/electrochemistry.py +176 -0
  18. quantized/calc/element_data.json +1 -0
  19. quantized/calc/element_data.py +59 -0
  20. quantized/calc/errors.py +246 -0
  21. quantized/calc/figure.py +417 -0
  22. quantized/calc/figure_break.py +152 -0
  23. quantized/calc/figure_categorical.py +156 -0
  24. quantized/calc/figure_corner.py +229 -0
  25. quantized/calc/figure_facets.py +127 -0
  26. quantized/calc/figure_field.py +137 -0
  27. quantized/calc/figure_hitmap.py +116 -0
  28. quantized/calc/figure_labels.py +62 -0
  29. quantized/calc/figure_map.py +287 -0
  30. quantized/calc/figure_overrides.py +159 -0
  31. quantized/calc/figure_page.py +266 -0
  32. quantized/calc/figure_scale.py +125 -0
  33. quantized/calc/figure_statplots.py +167 -0
  34. quantized/calc/figure_styles.py +131 -0
  35. quantized/calc/figure_ternary.py +239 -0
  36. quantized/calc/figure_ticks.py +217 -0
  37. quantized/calc/fit_autoguess.py +156 -0
  38. quantized/calc/fit_bootstrap.py +163 -0
  39. quantized/calc/fit_bumps.py +258 -0
  40. quantized/calc/fit_constraints.py +114 -0
  41. quantized/calc/fit_equation.py +264 -0
  42. quantized/calc/fit_findxy.py +80 -0
  43. quantized/calc/fit_models.py +195 -0
  44. quantized/calc/fit_models_special.py +189 -0
  45. quantized/calc/fit_odr.py +99 -0
  46. quantized/calc/fit_scan.py +243 -0
  47. quantized/calc/fit_stats.py +215 -0
  48. quantized/calc/fitting.py +199 -0
  49. quantized/calc/formula.py +90 -0
  50. quantized/calc/global_curve_fit.py +305 -0
  51. quantized/calc/global_fit.py +181 -0
  52. quantized/calc/interp2d.py +260 -0
  53. quantized/calc/linecut.py +269 -0
  54. quantized/calc/magnetic.py +414 -0
  55. quantized/calc/magnetometry.py +464 -0
  56. quantized/calc/map.py +228 -0
  57. quantized/calc/mcmc.py +177 -0
  58. quantized/calc/optics.py +228 -0
  59. quantized/calc/pawley.py +251 -0
  60. quantized/calc/peak_batch.py +128 -0
  61. quantized/calc/peak_fit.py +259 -0
  62. quantized/calc/peak_integrate.py +104 -0
  63. quantized/calc/peak_multifit.py +260 -0
  64. quantized/calc/peak_track.py +134 -0
  65. quantized/calc/peaks.py +298 -0
  66. quantized/calc/peakshapes.py +85 -0
  67. quantized/calc/plotting.py +147 -0
  68. quantized/calc/processing.py +232 -0
  69. quantized/calc/qspace.py +48 -0
  70. quantized/calc/reductions.py +155 -0
  71. quantized/calc/reductions_fft.py +383 -0
  72. quantized/calc/refl_sld_presets.json +1 -0
  73. quantized/calc/reflectivity.py +80 -0
  74. quantized/calc/registry.py +303 -0
  75. quantized/calc/relaxation.py +119 -0
  76. quantized/calc/report.py +253 -0
  77. quantized/calc/report_emit.py +227 -0
  78. quantized/calc/resample.py +142 -0
  79. quantized/calc/rsm.py +91 -0
  80. quantized/calc/rsm_analyze.py +245 -0
  81. quantized/calc/semiconductor.py +488 -0
  82. quantized/calc/sld.py +131 -0
  83. quantized/calc/sld_formula.py +138 -0
  84. quantized/calc/spectral.py +357 -0
  85. quantized/calc/statplots.py +214 -0
  86. quantized/calc/stats.py +399 -0
  87. quantized/calc/stats_anova2.py +202 -0
  88. quantized/calc/stats_anova_ext.py +338 -0
  89. quantized/calc/stats_dist.py +196 -0
  90. quantized/calc/stats_glm.py +245 -0
  91. quantized/calc/stats_multivar.py +289 -0
  92. quantized/calc/stats_roc.py +157 -0
  93. quantized/calc/stats_survival.py +261 -0
  94. quantized/calc/stats_tests.py +380 -0
  95. quantized/calc/substrates.py +181 -0
  96. quantized/calc/superconductor.py +359 -0
  97. quantized/calc/surface_fit.py +290 -0
  98. quantized/calc/surface_models.py +156 -0
  99. quantized/calc/thermal.py +119 -0
  100. quantized/calc/thin_film.py +425 -0
  101. quantized/calc/unit_convert.py +259 -0
  102. quantized/calc/units.py +80 -0
  103. quantized/calc/vacuum.py +290 -0
  104. quantized/calc/xray.py +169 -0
  105. quantized/cli.py +214 -0
  106. quantized/datastruct.py +153 -0
  107. quantized/io/__init__.py +11 -0
  108. quantized/io/_hdf5_layout.py +308 -0
  109. quantized/io/_jcamp_asdf.py +135 -0
  110. quantized/io/_xrdml_scan.py +291 -0
  111. quantized/io/base.py +82 -0
  112. quantized/io/bruker_brml.py +177 -0
  113. quantized/io/bruker_raw.py +158 -0
  114. quantized/io/cif.py +266 -0
  115. quantized/io/consolidated.py +122 -0
  116. quantized/io/delimited.py +222 -0
  117. quantized/io/excel.py +135 -0
  118. quantized/io/hdf5.py +192 -0
  119. quantized/io/import_filters.py +178 -0
  120. quantized/io/import_preview.py +262 -0
  121. quantized/io/jcamp.py +179 -0
  122. quantized/io/lakeshore.py +163 -0
  123. quantized/io/ncnr.py +278 -0
  124. quantized/io/netcdf.py +195 -0
  125. quantized/io/opus.py +231 -0
  126. quantized/io/origin.py +346 -0
  127. quantized/io/origin_com.py +194 -0
  128. quantized/io/origin_project/__init__.py +221 -0
  129. quantized/io/origin_project/annotation_marks.py +288 -0
  130. quantized/io/origin_project/container.py +262 -0
  131. quantized/io/origin_project/curve_style_color.py +359 -0
  132. quantized/io/origin_project/figure_geometry.py +108 -0
  133. quantized/io/origin_project/figure_layers.py +333 -0
  134. quantized/io/origin_project/figure_text.py +258 -0
  135. quantized/io/origin_project/figures.py +210 -0
  136. quantized/io/origin_project/figures_opju.py +440 -0
  137. quantized/io/origin_project/notes.py +302 -0
  138. quantized/io/origin_project/opj.py +459 -0
  139. quantized/io/origin_project/opj_curves.py +297 -0
  140. quantized/io/origin_project/opj_shapes.py +148 -0
  141. quantized/io/origin_project/opju.py +146 -0
  142. quantized/io/origin_project/opju_axis_real_form.py +418 -0
  143. quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
  144. quantized/io/origin_project/opju_codec.py +370 -0
  145. quantized/io/origin_project/opju_curves.py +497 -0
  146. quantized/io/origin_project/opju_curves_allcols.py +258 -0
  147. quantized/io/origin_project/opju_figure_curves.py +302 -0
  148. quantized/io/origin_project/opju_figure_text.py +245 -0
  149. quantized/io/origin_project/opju_reports.py +129 -0
  150. quantized/io/origin_project/origin_richtext.py +145 -0
  151. quantized/io/origin_project/preview.py +132 -0
  152. quantized/io/origin_project/templates.py +314 -0
  153. quantized/io/origin_project/tree.py +379 -0
  154. quantized/io/origin_project/tree_opju.py +228 -0
  155. quantized/io/origin_project/windows.py +238 -0
  156. quantized/io/origin_project/windows_opju.py +393 -0
  157. quantized/io/origin_project/writer.py +156 -0
  158. quantized/io/origin_project/writer_blocks.py +282 -0
  159. quantized/io/qd.py +380 -0
  160. quantized/io/refl1d.py +132 -0
  161. quantized/io/registry.py +210 -0
  162. quantized/io/report_export.py +347 -0
  163. quantized/io/rigaku.py +100 -0
  164. quantized/io/sims.py +398 -0
  165. quantized/io/spc.py +311 -0
  166. quantized/io/xrd_csv.py +308 -0
  167. quantized/io/xrdml.py +394 -0
  168. quantized/jobs.py +173 -0
  169. quantized/plugins/__init__.py +50 -0
  170. quantized/plugins/contract.py +111 -0
  171. quantized/plugins/loader.py +394 -0
  172. quantized/plugins/steps.py +90 -0
  173. quantized/routes/__init__.py +7 -0
  174. quantized/routes/_bookcache.py +62 -0
  175. quantized/routes/_export_common.py +27 -0
  176. quantized/routes/_payload.py +58 -0
  177. quantized/routes/_uploadcache.py +59 -0
  178. quantized/routes/aggregate.py +46 -0
  179. quantized/routes/baseline.py +210 -0
  180. quantized/routes/books.py +117 -0
  181. quantized/routes/calc.py +56 -0
  182. quantized/routes/corrections.py +78 -0
  183. quantized/routes/crystallography.py +80 -0
  184. quantized/routes/diffusion.py +58 -0
  185. quantized/routes/electrical.py +101 -0
  186. quantized/routes/electrochemistry.py +83 -0
  187. quantized/routes/export.py +280 -0
  188. quantized/routes/export_facets.py +83 -0
  189. quantized/routes/export_figures.py +471 -0
  190. quantized/routes/export_page.py +125 -0
  191. quantized/routes/fitting.py +379 -0
  192. quantized/routes/fitting_bumps.py +97 -0
  193. quantized/routes/import_template.py +97 -0
  194. quantized/routes/import_wizard.py +150 -0
  195. quantized/routes/jobs_api.py +59 -0
  196. quantized/routes/magnetic.py +135 -0
  197. quantized/routes/magnetometry.py +133 -0
  198. quantized/routes/optics.py +98 -0
  199. quantized/routes/parsers.py +281 -0
  200. quantized/routes/peaks.py +184 -0
  201. quantized/routes/plot.py +103 -0
  202. quantized/routes/reductions.py +121 -0
  203. quantized/routes/reference.py +58 -0
  204. quantized/routes/reflectivity.py +91 -0
  205. quantized/routes/report_export.py +119 -0
  206. quantized/routes/rsm.py +136 -0
  207. quantized/routes/samples.py +32 -0
  208. quantized/routes/semiconductor.py +207 -0
  209. quantized/routes/sld.py +42 -0
  210. quantized/routes/spectral.py +54 -0
  211. quantized/routes/statplots.py +99 -0
  212. quantized/routes/stats.py +418 -0
  213. quantized/routes/stats_design.py +321 -0
  214. quantized/routes/substrates.py +46 -0
  215. quantized/routes/superconductor.py +139 -0
  216. quantized/routes/thermal.py +57 -0
  217. quantized/routes/thin_film.py +153 -0
  218. quantized/routes/vacuum.py +113 -0
  219. quantized/routes/xray.py +32 -0
  220. quantized/samples/demo_vsm.csv +42 -0
  221. quantized/server_launch.py +251 -0
  222. quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
  223. quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
  224. quantized/web/assets/index-BHmmCL-x.js +27 -0
  225. quantized/web/assets/index-BiZzN7J6.css +1 -0
  226. quantized/web/index.html +13 -0
  227. quantized/web/loading.html +69 -0
  228. quantized_lab-0.8.0.dist-info/METADATA +122 -0
  229. quantized_lab-0.8.0.dist-info/RECORD +233 -0
  230. quantized_lab-0.8.0.dist-info/WHEEL +4 -0
  231. quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
  232. quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
  233. 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
+ }