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,80 @@
1
+ """Inverse-evaluate a fitted curve: X -> Y, and Y -> X (ALL crossings). MAIN #15.
2
+
3
+ Pure calc layer. ``find_y`` is a direct model evaluation at one point.
4
+ ``find_x`` samples ``model_fcn(x, params)`` on a dense grid over
5
+ ``[x_min, x_max]``, brackets every sign change (or exact zero) of
6
+ ``y(x) - target``, and refines each bracket with ``scipy.optimize.brentq`` --
7
+ so a non-monotonic curve (e.g. a Gaussian) reports every crossing, not just
8
+ the first. ``model_fcn`` is a plain ``fcn(x, p) -> y`` callable, the same
9
+ shape used by both ``calc.fit_models.evaluate`` (registry models) and
10
+ ``calc.fit_equation.equation_model`` (saved custom equations) -- so this
11
+ module works identically for either, and the route layer only has to pick
12
+ which one produced the callable.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Callable, Sequence
18
+
19
+ import numpy as np
20
+ from numpy.typing import NDArray
21
+ from scipy.optimize import brentq
22
+
23
+ __all__ = ["ModelFn", "find_x", "find_y"]
24
+
25
+ ModelFn = Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
26
+
27
+
28
+ def find_y(fcn: ModelFn, params: Sequence[float], x: float) -> float:
29
+ """Evaluate the fitted model at a single x."""
30
+ p = np.asarray(params, dtype=float)
31
+ y = fcn(np.asarray([x], dtype=float), p)
32
+ return float(np.asarray(y, dtype=float).ravel()[0])
33
+
34
+
35
+ def find_x(
36
+ fcn: ModelFn,
37
+ params: Sequence[float],
38
+ target: float,
39
+ x_min: float,
40
+ x_max: float,
41
+ *,
42
+ grid_points: int = 2000,
43
+ ) -> list[float]:
44
+ """All x in [x_min, x_max] where ``fcn(x, params) == target``.
45
+
46
+ Returns crossings in ascending order; an empty list (not an error) when
47
+ the curve never reaches ``target`` over the range -- e.g. a monotonic
48
+ curve entirely above or below it.
49
+ """
50
+ if x_max <= x_min:
51
+ raise ValueError("x_max must be greater than x_min")
52
+ if grid_points < 2:
53
+ raise ValueError("grid_points must be at least 2")
54
+ p = np.asarray(params, dtype=float)
55
+ xs = np.linspace(x_min, x_max, grid_points)
56
+ ys = np.asarray(fcn(xs, p), dtype=float).ravel()
57
+ g = ys - target
58
+
59
+ def g_at(xv: float) -> float:
60
+ yv = fcn(np.asarray([xv], dtype=float), p)
61
+ return float(np.asarray(yv, dtype=float).ravel()[0]) - target
62
+
63
+ # Grid spacing sets the merge tolerance below: a root that lands exactly
64
+ # on a grid node is found once via the `g0 == 0` branch, so the following
65
+ # bracket (whose left endpoint is that same node) must not re-report it.
66
+ dx_tol = (x_max - x_min) / (grid_points - 1) * 1e-6
67
+ roots: list[float] = []
68
+ for i in range(grid_points - 1):
69
+ g0, g1 = float(g[i]), float(g[i + 1])
70
+ if not (np.isfinite(g0) and np.isfinite(g1)):
71
+ continue # degenerate model params (e.g. div-by-zero) -- skip
72
+ if g0 == 0.0:
73
+ if not roots or xs[i] - roots[-1] > dx_tol:
74
+ roots.append(float(xs[i]))
75
+ continue
76
+ if g0 * g1 < 0.0:
77
+ roots.append(brentq(g_at, float(xs[i]), float(xs[i + 1])))
78
+ if np.isfinite(g[-1]) and g[-1] == 0.0 and (not roots or xs[-1] - roots[-1] > dx_tol):
79
+ roots.append(float(xs[-1]))
80
+ return roots
@@ -0,0 +1,195 @@
1
+ """Curve-fit model library. Port of fitting.models (the model evaluators).
2
+
3
+ Pure calc layer. Each model is ``f(x, p) -> y``; ``FIT_MODELS`` maps a model name
4
+ to its evaluator plus metadata (p0 / lower / upper bounds / paramNames). Closed-
5
+ form models live here; helper-based magnetic/thermal models (Langevin, Brillouin,
6
+ Stoner-Wohlfarth, Debye, Einstein) are registered in ``fit_models_special``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ from collections.abc import Callable
13
+ from typing import Any
14
+
15
+ import numpy as np
16
+ from numpy.typing import ArrayLike, NDArray
17
+
18
+ from .peakshapes import pseudo_voigt
19
+
20
+ __all__ = ["FIT_MODELS", "evaluate", "model_names", "register_model", "unregister_model"]
21
+
22
+ _INF = float("inf")
23
+ _EPS = float(np.finfo(float).eps)
24
+ _FWHM_PER_SIGMA = 2.0 * math.sqrt(2.0 * math.log(2.0))
25
+
26
+ ModelFn = Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
27
+
28
+ FIT_MODELS: dict[str, dict[str, Any]] = {}
29
+
30
+
31
+ def register_model(
32
+ name: str, category: str, fcn: ModelFn, param_names: list[str],
33
+ p0: list[float], lb: list[float], ub: list[float],
34
+ ) -> None:
35
+ FIT_MODELS[name] = {
36
+ "fcn": fcn, "category": category, "paramNames": param_names,
37
+ "p0": p0, "lb": lb, "ub": ub, "nParams": len(p0),
38
+ }
39
+
40
+
41
+ def unregister_model(name: str) -> None:
42
+ """Remove a registered model (used to unload plugin-contributed models)."""
43
+ FIT_MODELS.pop(name, None)
44
+
45
+
46
+ def evaluate(name: str, x: ArrayLike, p: ArrayLike) -> NDArray[np.float64]:
47
+ """Evaluate model ``name`` at points ``x`` with parameters ``p``."""
48
+ fcn: ModelFn = FIT_MODELS[name]["fcn"]
49
+ return np.asarray(fcn(np.asarray(x, dtype=float), np.asarray(p, dtype=float)), dtype=float)
50
+
51
+
52
+ def model_names() -> list[str]:
53
+ return list(FIT_MODELS)
54
+
55
+
56
+ # ── Linear / polynomial ─────────────────────────────────────────────────
57
+ def _linear(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
58
+ return np.asarray(p[0] * x + p[1], dtype=float)
59
+
60
+
61
+ def _quadratic(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
62
+ return np.asarray(p[0] * x**2 + p[1] * x + p[2], dtype=float)
63
+
64
+
65
+ def _cubic(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
66
+ return np.asarray(p[0] * x**3 + p[1] * x**2 + p[2] * x + p[3], dtype=float)
67
+
68
+
69
+ def _poly4(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
70
+ return np.asarray(p[0] * x**4 + p[1] * x**3 + p[2] * x**2 + p[3] * x + p[4], dtype=float)
71
+
72
+
73
+ # ── Decay / growth ──────────────────────────────────────────────────────
74
+ def _exp_decay(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
75
+ return np.asarray(p[0] * np.exp(-x / p[1]) + p[2], dtype=float)
76
+
77
+
78
+ def _stretched_exp(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
79
+ return np.asarray(p[0] * np.exp(-((x / p[1]) ** p[2])) + p[3], dtype=float)
80
+
81
+
82
+ def _biexp_decay(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
83
+ return np.asarray(p[0] * np.exp(-x / p[1]) + p[2] * np.exp(-x / p[3]) + p[4], dtype=float)
84
+
85
+
86
+ def _exp_growth(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
87
+ return np.asarray(p[0] * np.exp(x / p[1]) + p[2], dtype=float)
88
+
89
+
90
+ def _sat_growth(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
91
+ return np.asarray(p[0] * (1 - np.exp(-x / p[1])) + p[2], dtype=float)
92
+
93
+
94
+ # ── Peak shapes ─────────────────────────────────────────────────────────
95
+ def _gaussian(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
96
+ return np.asarray(p[0] * np.exp(-((x - p[1]) ** 2) / (2 * p[2] ** 2)), dtype=float)
97
+
98
+
99
+ def _lorentzian(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
100
+ return np.asarray(p[0] / (1 + ((x - p[1]) / p[2]) ** 2), dtype=float)
101
+
102
+
103
+ def _pseudo_voigt(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
104
+ return pseudo_voigt(x, float(p[1]), _FWHM_PER_SIGMA * float(p[2]), float(p[0]), float(p[3]))
105
+
106
+
107
+ # ── Power / sigmoid / misc ──────────────────────────────────────────────
108
+ def _power_law(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
109
+ return np.asarray(p[0] * np.abs(x) ** p[1] + p[2], dtype=float)
110
+
111
+
112
+ def _allometric(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
113
+ return np.asarray(p[0] * np.abs(x) ** p[1], dtype=float)
114
+
115
+
116
+ def _logistic(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
117
+ return np.asarray(p[0] / (1 + np.exp(-p[1] * (x - p[2]))) + p[3], dtype=float)
118
+
119
+
120
+ def _tanh(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
121
+ return np.asarray(p[0] * np.tanh(p[1] * (x - p[2])) + p[3], dtype=float)
122
+
123
+
124
+ def _curie_weiss(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
125
+ return np.asarray(p[0] / (x - p[1]), dtype=float)
126
+
127
+
128
+ def _bloch(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
129
+ return np.asarray(p[0] * (1 - p[1] * x**1.5), dtype=float)
130
+
131
+
132
+ def _arrhenius(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
133
+ return np.asarray(p[0] * np.exp(-p[1] / x), dtype=float)
134
+
135
+
136
+ def _vft(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
137
+ return np.asarray(p[0] * np.exp(p[1] / (8.617e-5 * (x - p[2]))), dtype=float)
138
+
139
+
140
+ def _langmuir(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
141
+ return np.asarray(p[0] * x / (p[1] + x), dtype=float)
142
+
143
+
144
+ def _logarithmic(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
145
+ return np.asarray(p[0] * np.log(np.abs(x) + _EPS) + p[1], dtype=float)
146
+
147
+
148
+ def _sqrt(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
149
+ return np.asarray(p[0] * np.sqrt(np.abs(x)) + p[1], dtype=float)
150
+
151
+
152
+ # ── Registry (closed-form models) ───────────────────────────────────────
153
+ register_model("Linear", "Linear", _linear, ["m", "b"], [1, 0], [-_INF, -_INF], [_INF, _INF])
154
+ register_model("Quadratic", "Linear", _quadratic, ["a", "b", "c"], [0, 1, 0],
155
+ [-_INF, -_INF, -_INF], [_INF, _INF, _INF])
156
+ register_model("Cubic", "Linear", _cubic, ["a", "b", "c", "d"], [0, 0, 1, 0],
157
+ [-_INF] * 4, [_INF] * 4)
158
+ register_model("Poly 4", "Linear", _poly4, ["a", "b", "c", "d", "e"], [0, 0, 0, 1, 0],
159
+ [-_INF] * 5, [_INF] * 5)
160
+ register_model("Exponential Decay", "Decay", _exp_decay, ["A", "τ", "C"], [1, 1, 0],
161
+ [-_INF, 0, -_INF], [_INF, _INF, _INF])
162
+ register_model("Stretched Exponential", "Decay", _stretched_exp, ["A", "τ", "β", "C"],
163
+ [1, 1, 0.5, 0], [-_INF, 0, 0, -_INF], [_INF, _INF, 2, _INF])
164
+ register_model("Bi-exponential Decay", "Decay", _biexp_decay, ["A₁", "τ₁", "A₂", "τ₂", "C"],
165
+ [1, 1, 0.5, 5, 0], [-_INF, 0, -_INF, 0, -_INF], [_INF] * 5)
166
+ register_model("Exponential Growth", "Growth", _exp_growth, ["A", "τ", "C"], [1, 1, 0],
167
+ [-_INF, 0, -_INF], [_INF, _INF, _INF])
168
+ register_model("Saturation Growth", "Growth", _sat_growth, ["A", "τ", "C"], [1, 1, 0],
169
+ [-_INF, 0, -_INF], [_INF, _INF, _INF])
170
+ register_model("Gaussian", "Peak", _gaussian, ["A", "μ", "σ"], [1, 0, 1],
171
+ [-_INF, -_INF, 0], [_INF, _INF, _INF])
172
+ register_model("Lorentzian", "Peak", _lorentzian, ["A", "x₀", "γ"], [1, 0, 1],
173
+ [-_INF, -_INF, 0], [_INF, _INF, _INF])
174
+ register_model("Pseudo-Voigt", "Peak", _pseudo_voigt, ["A", "x₀", "w", "η"], [1, 0, 1, 0.5],
175
+ [-_INF, -_INF, 0, 0], [_INF, _INF, _INF, 1])
176
+ register_model("Power Law", "Power", _power_law, ["A", "n", "C"], [1, 1, 0],
177
+ [-_INF, -_INF, -_INF], [_INF, _INF, _INF])
178
+ register_model("Allometric", "Power", _allometric, ["A", "n"], [1, 1], [-_INF, -_INF], [_INF, _INF])
179
+ register_model("Logistic", "Sigmoid", _logistic, ["A", "k", "x₀", "C"], [1, 1, 0, 0],
180
+ [-_INF, 0, -_INF, -_INF], [_INF, _INF, _INF, _INF])
181
+ register_model("Tanh", "Sigmoid", _tanh, ["A", "k", "x₀", "C"], [1, 1, 0, 0],
182
+ [-_INF, 0, -_INF, -_INF], [_INF, _INF, _INF, _INF])
183
+ register_model("Curie-Weiss", "Magnetic", _curie_weiss, ["C", "θ"], [1, 0],
184
+ [0, -_INF], [_INF, _INF])
185
+ register_model("Bloch T^3/2", "Magnetic", _bloch, ["M₀", "B"], [1, 1e-5], [0, 0], [_INF, _INF])
186
+ register_model("Arrhenius", "Thermal", _arrhenius, ["A", "Eₐ/kB"], [1, 1000], [0, 0], [_INF, _INF])
187
+ register_model("VFT", "Decay", _vft, ["τ₀", "Ea_eV", "T₀"], [1e-10, 0.05, 0], [0, 0, 0],
188
+ [1, 10, _INF])
189
+ register_model("Langmuir", "Thermal", _langmuir, ["A", "K"], [1, 1], [0, 0], [_INF, _INF])
190
+ register_model("Logarithmic", "Other", _logarithmic, ["a", "b"], [1, 0], [-_INF, -_INF],
191
+ [_INF, _INF])
192
+ register_model("Square Root", "Other", _sqrt, ["a", "b"], [1, 0], [-_INF, -_INF], [_INF, _INF])
193
+
194
+ # Register the helper-based magnetic/heat-capacity models (side-effect import).
195
+ from . import fit_models_special # noqa: E402, F401
@@ -0,0 +1,189 @@
1
+ """Helper-based fitting models (magnetic + heat-capacity). Extends fit_models.
2
+
3
+ Imported for its registration side effects (see the import at the bottom of
4
+ fit_models.py). Models: Langevin, Brillouin, Stoner-Wohlfarth, Debye, Einstein,
5
+ Debye+Einstein. Port of the local helpers in fitting/models.m.
6
+
7
+ The ``Hysteresis``-category models (tanh loop, two-component F+P, linear
8
+ background, approach-to-saturation, Langevin+background) are a port of
9
+ ``+fitting/hysteresisModels.m`` — the catalogue the magnetometry hysteresis
10
+ workshop offers for M-H loop fitting. They are empirical loop descriptors, not
11
+ full Stoner-Wohlfarth astroid solutions (see Cullity & Graham, "Introduction to
12
+ Magnetic Materials", 2nd ed., Ch. 7/9/11; Akulov, Z. Phys. 67, 794 (1931)).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+
19
+ import numpy as np
20
+ from numpy.typing import NDArray
21
+ from scipy.integrate import quad
22
+
23
+ from .fit_models import register_model
24
+
25
+ _INF = float("inf")
26
+ _EPS = float(np.finfo(float).eps)
27
+ _R = 8.314 # molar gas constant J/(mol·K)
28
+ _DEBYE_LIMIT = 4 * math.pi**4 / 15 # integral_0^inf x^4 e^x/(e^x-1)^2 dx
29
+
30
+
31
+ def _langevin(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
32
+ a, b = float(p[0]), float(p[1])
33
+ u = x / max(b, _EPS)
34
+ out = np.empty_like(u, dtype=float)
35
+ small = np.abs(u) < 1e-4
36
+ out[small] = a * (u[small] / 3 - u[small] ** 3 / 45)
37
+ us = u[~small]
38
+ out[~small] = a * (1.0 / np.tanh(us) - 1.0 / us)
39
+ return np.asarray(out, dtype=float)
40
+
41
+
42
+ def _brillouin_bj(j: float, y: NDArray[np.float64]) -> NDArray[np.float64]:
43
+ if j == 0:
44
+ return np.zeros_like(y, dtype=float)
45
+ a = (2 * j + 1) / (2 * j)
46
+ b = 1 / (2 * j)
47
+ out = np.empty_like(y, dtype=float)
48
+ small = np.abs(y) < 1e-6
49
+ out[small] = (j + 1) / (3 * j) * y[small]
50
+ yl = y[~small]
51
+ out[~small] = a / np.tanh(a * yl) - b / np.tanh(b * yl)
52
+ return out
53
+
54
+
55
+ def _brillouin(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
56
+ ms, j, g, t = float(p[0]), float(p[1]), float(p[2]), float(p[3])
57
+ y = g * 5.7884e-5 * j * x / (8.617e-5 * t)
58
+ return np.asarray(ms * _brillouin_bj(j, y), dtype=float)
59
+
60
+
61
+ def _stoner_wohlfarth(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
62
+ ms, hc = float(p[0]), float(p[1])
63
+ hk = max(float(p[2]), _EPS)
64
+ heff = x - np.sign(x) * hc
65
+ return np.asarray(ms * np.tanh(heff / hk), dtype=float)
66
+
67
+
68
+ def _debye_integrand(t: float) -> float:
69
+ et = math.exp(t)
70
+ return t**4 * et / max((et - 1) ** 2, _EPS)
71
+
72
+
73
+ def _debye_integral(u: float) -> float:
74
+ if u > 30:
75
+ return _DEBYE_LIMIT
76
+ if u < 1e-4:
77
+ return u**3 / 3
78
+ val, _ = quad(_debye_integrand, 0.0, u, epsrel=1e-6, epsabs=1e-10)
79
+ return float(val)
80
+
81
+
82
+ def _debye(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
83
+ gamma, theta, n = float(p[0]), max(float(p[1]), 1.0), max(float(p[2]), 0.0)
84
+ t = np.asarray(x, dtype=float).ravel()
85
+ out = np.empty(t.size)
86
+ for k in range(t.size):
87
+ tk = max(float(t[k]), 0.01)
88
+ u = theta / tk
89
+ c_lat = 9 * _R * (1 / u) ** 3 * _debye_integral(u)
90
+ out[k] = gamma * tk + n * c_lat * 1000
91
+ return out
92
+
93
+
94
+ def _einstein_lattice(theta: float, tk: float) -> float:
95
+ u = theta / tk
96
+ eu = math.exp(min(u, 500))
97
+ return 3 * _R * u**2 * eu / max((eu - 1) ** 2, _EPS)
98
+
99
+
100
+ def _einstein(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
101
+ gamma, theta, n = float(p[0]), max(float(p[1]), 1.0), max(float(p[2]), 0.0)
102
+ t = np.asarray(x, dtype=float).ravel()
103
+ out = np.empty(t.size)
104
+ for k in range(t.size):
105
+ tk = max(float(t[k]), 0.01)
106
+ out[k] = gamma * tk + n * _einstein_lattice(theta, tk) * 1000
107
+ return out
108
+
109
+
110
+ def _debye_einstein(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
111
+ gamma = float(p[0])
112
+ theta_d, n_d = max(float(p[1]), 1.0), max(float(p[2]), 0.0)
113
+ theta_e, n_e = max(float(p[3]), 1.0), max(float(p[4]), 0.0)
114
+ t = np.asarray(x, dtype=float).ravel()
115
+ out = np.empty(t.size)
116
+ for k in range(t.size):
117
+ tk = max(float(t[k]), 0.01)
118
+ c_d = 9 * _R * (1 / (theta_d / tk)) ** 3 * _debye_integral(theta_d / tk)
119
+ c_e = _einstein_lattice(theta_e, tk)
120
+ out[k] = gamma * tk + (n_d * c_d + n_e * c_e) * 1000
121
+ return out
122
+
123
+
124
+ # ── Hysteresis (magnetic M-H loop descriptors) ──────────────────────────
125
+ def _tanh_hysteresis(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
126
+ """M = Ms·tanh((H - Hc)/Hw). Soft-ferromagnet loop. p = [Ms, Hc, Hw]."""
127
+ ms, hc, hw = float(p[0]), float(p[1]), max(abs(float(p[2])), _EPS)
128
+ return np.asarray(ms * np.tanh((x - hc) / hw), dtype=float)
129
+
130
+
131
+ def _two_component(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
132
+ """M = Ms·tanh((H - Hc)/Hw) + χ·H. Ferromagnetic loop + linear (para) BG.
133
+
134
+ p = [Ms, Hc, Hw, χ].
135
+ """
136
+ ms, hc, hw, chi = float(p[0]), float(p[1]), max(abs(float(p[2])), _EPS), float(p[3])
137
+ return np.asarray(ms * np.tanh((x - hc) / hw) + chi * x, dtype=float)
138
+
139
+
140
+ def _linear_background(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
141
+ """M = χ·H + offset. Pure linear (dia/paramagnetic) background. p = [χ, offset]."""
142
+ return np.asarray(float(p[0]) * x + float(p[1]), dtype=float)
143
+
144
+
145
+ def _approach_saturation(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
146
+ """M = Ms(1 - a/|H| - b/H²) + χ·H. High-field Akulov expansion. p = [Ms, a, b, χ]."""
147
+ ms, a, b, chi = float(p[0]), float(p[1]), float(p[2]), float(p[3])
148
+ xv = np.asarray(x, dtype=float)
149
+ return np.asarray(
150
+ ms * (1.0 - a / (np.abs(xv) + _EPS) - b / (xv**2 + _EPS)) + chi * xv, dtype=float
151
+ )
152
+
153
+
154
+ def _langevin_bg(x: NDArray[np.float64], p: NDArray[np.float64]) -> NDArray[np.float64]:
155
+ """M = Ms·L(αH) + χ·H, L(u) = coth(u) - 1/u. Superparamagnet + BG. p = [Ms, α, χ]."""
156
+ ms, alpha, chi = float(p[0]), float(p[1]), float(p[2])
157
+ u = alpha * np.asarray(x, dtype=float)
158
+ out = np.empty_like(u, dtype=float)
159
+ small = np.abs(u) < 1e-4
160
+ out[small] = ms * (u[small] / 3.0 - u[small] ** 3 / 45.0)
161
+ us = u[~small]
162
+ out[~small] = ms * (1.0 / np.tanh(us) - 1.0 / us)
163
+ return np.asarray(out + chi * np.asarray(x, dtype=float), dtype=float)
164
+
165
+
166
+ register_model("Tanh Hysteresis", "Hysteresis", _tanh_hysteresis, ["Ms", "Hc", "Hw"],
167
+ [1e-3, 100, 200], [0, -_INF, 0], [_INF, _INF, _INF])
168
+ register_model("Two-Component (F+P)", "Hysteresis", _two_component, ["Ms", "Hc", "Hw", "χ"],
169
+ [1e-3, 100, 200, 0], [0, -_INF, 0, -_INF], [_INF, _INF, _INF, _INF])
170
+ register_model("Linear Background", "Hysteresis", _linear_background, ["χ", "offset"],
171
+ [1e-7, 0], [-_INF, -_INF], [_INF, _INF])
172
+ register_model("Approach to Saturation", "Hysteresis", _approach_saturation,
173
+ ["Ms", "a", "b", "χ"], [1e-3, 1, 1, 0], [0, 0, 0, -_INF],
174
+ [_INF, _INF, _INF, _INF])
175
+ register_model("Langevin + Background", "Hysteresis", _langevin_bg, ["Ms", "μ/kT", "χ"],
176
+ [1e-3, 1e-3, 0], [0, 0, -_INF], [_INF, _INF, _INF])
177
+
178
+ register_model("Langevin", "Magnetic", _langevin, ["A", "B"], [1, 1], [0, 0], [_INF, _INF])
179
+ register_model("Brillouin", "Magnetic", _brillouin, ["Ms", "J", "g", "T"], [1, 0.5, 2, 300],
180
+ [0, 0.5, 0, 0], [_INF, 7, 10, 1000])
181
+ register_model("Stoner-Wohlfarth", "Magnetic", _stoner_wohlfarth, ["Ms", "Hc", "Hk"],
182
+ [1, 100, 500], [0, 0, 0], [_INF, _INF, _INF])
183
+ register_model("Debye", "Thermal", _debye, ["gamma", "thetaD", "n"], [5, 200, 1],
184
+ [0, 1, 0.1], [_INF, _INF, 20])
185
+ register_model("Einstein", "Thermal", _einstein, ["gamma", "thetaE", "n"], [5, 150, 1],
186
+ [0, 1, 0.1], [_INF, _INF, 20])
187
+ register_model("Debye+Einstein", "Thermal", _debye_einstein,
188
+ ["gamma", "thetaD", "n_D", "thetaE", "n_E"], [5, 200, 0.8, 150, 0.2],
189
+ [0, 1, 0, 1, 0], [_INF, _INF, 20, _INF, 20])
@@ -0,0 +1,99 @@
1
+ """Orthogonal distance (Deming) regression. Port of fitting.odrFit.
2
+
3
+ Pure calc layer. Linear fit ``y = slope·x + intercept`` minimising the squared
4
+ *perpendicular* distances (errors in both x and y), with a variance ratio
5
+ ``lambda = σy²/σx²`` (λ→∞ → OLS, λ→0 → inverse OLS, λ=1 → symmetric ODR). The
6
+ estimator is closed-form (Deming); standard errors come from jackknife
7
+ leave-one-out refits — exactly as the MATLAB original.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ from typing import Any
14
+
15
+ import numpy as np
16
+ from numpy.typing import ArrayLike, NDArray
17
+
18
+ __all__ = ["odr_fit"]
19
+
20
+ _EPS = float(np.finfo(float).eps)
21
+
22
+
23
+ def _deming_fit(x: NDArray[np.float64], y: NDArray[np.float64], lam: float) -> tuple[float, float]:
24
+ """Closed-form Deming regression on centered moments (port of demingFit)."""
25
+ xbar, ybar = float(np.mean(x)), float(np.mean(y))
26
+ xc = x - xbar
27
+ yc = y - ybar
28
+ sxx = float(np.sum(xc**2))
29
+ syy = float(np.sum(yc**2))
30
+ sxy = float(np.sum(xc * yc))
31
+ if abs(sxy) < _EPS:
32
+ slope = 0.0 # no linear correlation → flat line anchored at the mean
33
+ else:
34
+ disc = (syy - lam * sxx) ** 2 + 4.0 * lam * sxy**2
35
+ slope = (syy - lam * sxx + math.copysign(1.0, sxy) * math.sqrt(disc)) / (2.0 * sxy)
36
+ intercept = ybar - slope * xbar
37
+ return slope, intercept
38
+
39
+
40
+ def odr_fit(
41
+ x: ArrayLike,
42
+ y: ArrayLike,
43
+ *,
44
+ lambda_: float = 1.0,
45
+ x_error: ArrayLike | None = None,
46
+ y_error: ArrayLike | None = None,
47
+ ) -> dict[str, Any]:
48
+ """Deming/orthogonal linear regression. Port of fitting.odrFit.
49
+
50
+ ``lambda_`` is the σy²/σx² ratio (default 1 → symmetric ODR). If both
51
+ ``x_error`` and ``y_error`` are given, λ is derived as
52
+ ``(mean(y_error)/mean(x_error))²``. Returns a dict with ``slope``/``intercept``,
53
+ jackknife ``slopeErr``/``interceptErr``, ``lambda``, ``rss``, ``rmse``, ``n``.
54
+ """
55
+ xv = np.asarray(x, dtype=float).ravel()
56
+ yv = np.asarray(y, dtype=float).ravel()
57
+ if xv.size != yv.size:
58
+ raise ValueError("x and y must have the same length")
59
+ n = xv.size
60
+ if n < 3:
61
+ raise ValueError(f"ODR requires at least 3 points (got {n})")
62
+
63
+ lam = float(lambda_)
64
+ if lam <= 0:
65
+ raise ValueError("lambda_ must be positive")
66
+ if x_error is not None and y_error is not None:
67
+ xe_mean = float(np.nanmean(np.asarray(x_error, dtype=float)))
68
+ ye_mean = float(np.nanmean(np.asarray(y_error, dtype=float)))
69
+ if xe_mean <= 0:
70
+ raise ValueError("mean x_error must be positive")
71
+ lam = (ye_mean / xe_mean) ** 2
72
+
73
+ slope, intercept = _deming_fit(xv, yv, lam)
74
+
75
+ # Orthogonal residuals: perpendicular distance from each point to the line.
76
+ res = (slope * xv - yv + intercept) / math.sqrt(slope**2 + 1.0)
77
+ rss = float(np.sum(res**2))
78
+ rmse = math.sqrt(rss / n)
79
+
80
+ # Jackknife standard errors — refit n times leaving one point out.
81
+ slopes = np.empty(n)
82
+ intercepts = np.empty(n)
83
+ for k in range(n):
84
+ mask = np.arange(n) != k
85
+ slopes[k], intercepts[k] = _deming_fit(xv[mask], yv[mask], lam)
86
+ factor = (n - 1) / n
87
+ slope_err = math.sqrt(factor * float(np.sum((slopes - np.mean(slopes)) ** 2)))
88
+ intercept_err = math.sqrt(factor * float(np.sum((intercepts - np.mean(intercepts)) ** 2)))
89
+
90
+ return {
91
+ "slope": slope,
92
+ "intercept": intercept,
93
+ "slopeErr": slope_err,
94
+ "interceptErr": intercept_err,
95
+ "lambda": lam,
96
+ "rss": rss,
97
+ "rmse": rmse,
98
+ "n": n,
99
+ }