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,290 @@
1
+ """Bounded 2D least-squares surface fitting. Port of MATLAB ``fitting.surfaceFit``.
2
+
3
+ Fits a named 2D model (``calc/surface_models``) to scattered ``(x, y, z)`` data.
4
+ The MATLAB original minimises the sum of squared residuals with **fminsearch
5
+ (Nelder-Mead)** over a **bounded → unbounded parameter transform** (the same
6
+ idiosyncratic scheme ``curveFit`` uses), so this port replicates the transform
7
+ and uses scipy's Nelder-Mead rather than delegating to a different bounded
8
+ optimiser. Parameter errors come from the numerical Hessian of the cost at the
9
+ optimum, mapped back through the transform Jacobian.
10
+
11
+ Pure calc layer — ndarray in -> dict out; no fastapi/pydantic. ``p0`` (initial
12
+ guess) is required here; the MATLAB auto-guess (``surfaceAutoGuess``) is a
13
+ separate, still-to-port file.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys
19
+ from collections.abc import Sequence
20
+ from typing import Any
21
+
22
+ import numpy as np
23
+ from numpy.typing import ArrayLike, NDArray
24
+ from scipy.optimize import minimize
25
+
26
+ from quantized.calc.surface_models import SurfaceModel, get_surface_model
27
+
28
+ __all__ = ["surface_fit", "surface_auto_guess"]
29
+
30
+ _EPS = sys.float_info.epsilon
31
+
32
+
33
+ def _weighted_centroid(
34
+ xa: NDArray[np.float64], ya: NDArray[np.float64], za: NDArray[np.float64], z_min: float
35
+ ) -> tuple[float, float]:
36
+ """Intensity-weighted (x, y) centre (weights = z - z_min, clamped >= 0)."""
37
+ wts = np.maximum(za - z_min, 0.0)
38
+ w_sum = max(float(np.sum(wts)), _EPS)
39
+ return float(np.sum(wts * xa) / w_sum), float(np.sum(wts * ya) / w_sum)
40
+
41
+
42
+ def surface_auto_guess(
43
+ model: str | SurfaceModel,
44
+ x: ArrayLike,
45
+ y: ArrayLike,
46
+ z: ArrayLike,
47
+ ) -> NDArray[np.float64]:
48
+ """Heuristic initial parameter guess for a 2D model. Port of
49
+ ``fitting.surfaceAutoGuess``: linear models solve normal equations; peak
50
+ models use ``z``-range amplitude, an intensity-weighted centroid, and
51
+ range/4 widths."""
52
+ mdl = get_surface_model(model) if isinstance(model, str) else model
53
+ xa = np.asarray(x, dtype=float).ravel()
54
+ ya = np.asarray(y, dtype=float).ravel()
55
+ za = np.asarray(z, dtype=float).ravel()
56
+ n = za.size
57
+
58
+ x_rng = max(float(xa.max() - xa.min()), _EPS)
59
+ y_rng = max(float(ya.max() - ya.min()), _EPS)
60
+ z_min = float(za.min())
61
+ z_rng = max(float(za.max() - z_min), _EPS)
62
+ z_mean = float(za.mean())
63
+
64
+ name = mdl.name
65
+ if name == "Plane":
66
+ amat = np.column_stack([xa, ya, np.ones(n)])
67
+ return _lstsq_or(amat, za, [0.0, 0.0, z_mean])
68
+ if name == "Paraboloid":
69
+ amat = np.column_stack([xa**2, ya**2, xa * ya, xa, ya, np.ones(n)])
70
+ return _lstsq_or(amat, za, [0.0, 0.0, 0.0, 0.0, 0.0, z_mean])
71
+ if name == "Polynomial 2D":
72
+ amat = np.column_stack([np.ones(n), xa, ya, xa**2, xa * ya, ya**2])
73
+ return _lstsq_or(amat, za, [z_mean, 0.0, 0.0, 0.0, 0.0, 0.0])
74
+ if name in ("2D Gaussian", "2D Lorentzian", "2D Pseudo-Voigt"):
75
+ amp = float(za.max()) - z_min
76
+ x0, y0 = _weighted_centroid(xa, ya, za, z_min)
77
+ guess = [amp, x0, x_rng / 4, y0, y_rng / 4, z_min]
78
+ if name == "2D Pseudo-Voigt":
79
+ guess.append(0.5) # eta
80
+ return np.asarray(guess, dtype=float)
81
+ if name == "Exponential Decay 2D":
82
+ return np.asarray([z_rng, x_rng / 3, y_rng / 3, z_min], dtype=float)
83
+
84
+ # Generic fallback for any other model.
85
+ out = np.ones(mdl.n_params, dtype=float)
86
+ defaults = [z_rng, (float(xa.min()) + float(xa.max())) / 2, x_rng / 4,
87
+ (float(ya.min()) + float(ya.max())) / 2, y_rng / 4, z_min]
88
+ for i, val in enumerate(defaults[: mdl.n_params]):
89
+ out[i] = val
90
+ return out
91
+
92
+
93
+ def _lstsq_or(
94
+ amat: NDArray[np.float64], z: NDArray[np.float64], fallback: list[float]
95
+ ) -> NDArray[np.float64]:
96
+ """Least-squares solve (MATLAB ``A \\ z``), falling back on a singular system."""
97
+ try:
98
+ coeffs, *_ = np.linalg.lstsq(amat, z, rcond=None)
99
+ return np.asarray(coeffs, dtype=float).ravel()
100
+ except np.linalg.LinAlgError:
101
+ return np.asarray(fallback, dtype=float)
102
+
103
+
104
+ def _bound_to_free(pb: float, lo: float, hi: float) -> float:
105
+ if lo == -np.inf and hi == np.inf:
106
+ return pb
107
+ if lo > -np.inf and hi == np.inf:
108
+ return float(np.log(pb - lo + _EPS))
109
+ if lo == -np.inf and hi < np.inf:
110
+ return float(-np.log(hi - pb + _EPS))
111
+ t = (pb - lo) / (hi - lo)
112
+ t = max(min(t, 1 - _EPS), _EPS)
113
+ return float(np.log(t / (1 - t)))
114
+
115
+
116
+ def _free_to_bound(pf: float, lo: float, hi: float) -> float:
117
+ if lo == -np.inf and hi == np.inf:
118
+ return pf
119
+ if lo > -np.inf and hi == np.inf:
120
+ return float(lo + np.exp(pf))
121
+ if lo == -np.inf and hi < np.inf:
122
+ return float(hi - np.exp(-pf))
123
+ return float(lo + (hi - lo) / (1 + np.exp(-pf)))
124
+
125
+
126
+ def _bound_jacobian(pf: float, lo: float, hi: float) -> float:
127
+ if lo == -np.inf and hi == np.inf:
128
+ return 1.0
129
+ if lo > -np.inf and hi == np.inf:
130
+ return float(np.exp(pf))
131
+ if lo == -np.inf and hi < np.inf:
132
+ return float(np.exp(-pf))
133
+ s = 1.0 / (1.0 + np.exp(-pf))
134
+ return float((hi - lo) * s * (1 - s))
135
+
136
+
137
+ def _numerical_hessian(fun: Any, x0: NDArray[np.float64]) -> NDArray[np.float64]:
138
+ """Central-difference Hessian (matches the MATLAB step ``max(|x|·1e-4, 1e-6)``)."""
139
+ n = x0.size
140
+ hess = np.zeros((n, n), dtype=float)
141
+ f0 = fun(x0)
142
+ h = np.maximum(np.abs(x0) * 1e-4, 1e-6)
143
+ for i in range(n):
144
+ xp = x0.copy()
145
+ xp[i] += h[i]
146
+ xm = x0.copy()
147
+ xm[i] -= h[i]
148
+ hess[i, i] = (fun(xp) - 2 * f0 + fun(xm)) / h[i] ** 2
149
+ for j in range(i + 1, n):
150
+ xpp, xpm, xmp, xmm = x0.copy(), x0.copy(), x0.copy(), x0.copy()
151
+ xpp[i] += h[i]
152
+ xpp[j] += h[j]
153
+ xpm[i] += h[i]
154
+ xpm[j] -= h[j]
155
+ xmp[i] -= h[i]
156
+ xmp[j] += h[j]
157
+ xmm[i] -= h[i]
158
+ xmm[j] -= h[j]
159
+ hess[i, j] = (fun(xpp) - fun(xpm) - fun(xmp) + fun(xmm)) / (4 * h[i] * h[j])
160
+ hess[j, i] = hess[i, j]
161
+ return hess
162
+
163
+
164
+ def _bounds(
165
+ bound: Sequence[float] | None, n: int, default: float, name: str
166
+ ) -> NDArray[np.float64]:
167
+ if bound is None:
168
+ return np.full(n, default, dtype=float)
169
+ arr = np.asarray(bound, dtype=float)
170
+ if arr.size != n:
171
+ raise ValueError(f"{name} must have {n} elements, got {arr.size}")
172
+ return arr
173
+
174
+
175
+ def surface_fit(
176
+ x: ArrayLike,
177
+ y: ArrayLike,
178
+ z: ArrayLike,
179
+ model: str | SurfaceModel,
180
+ *,
181
+ p0: Sequence[float] | None = None,
182
+ lower: Sequence[float] | None = None,
183
+ upper: Sequence[float] | None = None,
184
+ max_iter: int = 10000,
185
+ ) -> dict[str, Any]:
186
+ """Fit ``model`` to scattered ``(x, y, z)``.
187
+
188
+ ``p0`` is the initial guess; when omitted it is derived via
189
+ :func:`surface_auto_guess`. ``lower``/``upper`` bound each parameter
190
+ (defaults ``-inf``/``+inf``). Returns a dict with ``params``,
191
+ ``param_names``, ``errors`` (1-sigma, NaN if the Hessian is singular),
192
+ ``residuals``, ``z_fit``, ``r2``, ``rmse``, ``chi_sq_red``, ``model_name``,
193
+ ``n_points``, ``n_free``, ``exit_flag``.
194
+ """
195
+ mdl = get_surface_model(model) if isinstance(model, str) else model
196
+ xa = np.asarray(x, dtype=float).ravel()
197
+ ya = np.asarray(y, dtype=float).ravel()
198
+ za = np.asarray(z, dtype=float).ravel()
199
+ n_pts = za.size
200
+ n_p = mdl.n_params
201
+
202
+ if p0 is None:
203
+ p0_arr = surface_auto_guess(mdl, xa, ya, za)
204
+ else:
205
+ p0_arr = np.asarray(p0, dtype=float).ravel()
206
+ if p0_arr.size != n_p:
207
+ raise ValueError(f"p0 must have {n_p} elements for {mdl.name!r}, got {p0_arr.size}")
208
+ lb = _bounds(lower, n_p, -np.inf, "lower")
209
+ ub = _bounds(upper, n_p, np.inf, "upper")
210
+
211
+ p0_arr = np.clip(p0_arr, lb, ub)
212
+
213
+ def to_free(pb: NDArray[np.float64]) -> NDArray[np.float64]:
214
+ out = np.empty(n_p, dtype=float)
215
+ for k in range(n_p):
216
+ out[k] = _bound_to_free(float(pb[k]), float(lb[k]), float(ub[k]))
217
+ return out
218
+
219
+ def from_free(pf: NDArray[np.float64]) -> NDArray[np.float64]:
220
+ out = np.empty(n_p, dtype=float)
221
+ for k in range(n_p):
222
+ out[k] = _free_to_bound(float(pf[k]), float(lb[k]), float(ub[k]))
223
+ return out
224
+
225
+ def cost(p_free: NDArray[np.float64]) -> float:
226
+ p_full = from_free(p_free)
227
+ resid = za - mdl.func(p_full, xa, ya).ravel()
228
+ return float(np.sum(resid**2))
229
+
230
+ p_free0 = to_free(p0_arr)
231
+ res = minimize(
232
+ cost,
233
+ p_free0,
234
+ method="Nelder-Mead",
235
+ options={"xatol": 1e-10, "fatol": 1e-12, "maxiter": max_iter, "maxfev": max_iter * 2},
236
+ )
237
+ p_free_opt = np.asarray(res.x, dtype=float)
238
+ p_opt = from_free(p_free_opt)
239
+ z_fit = np.asarray(mdl.func(p_opt, xa, ya).ravel(), dtype=float)
240
+ residuals = za - z_fit
241
+
242
+ ss_res = float(np.sum(residuals**2))
243
+ ss_tot = float(np.sum((za - za.mean()) ** 2))
244
+ r2 = 1 - ss_res / max(ss_tot, _EPS)
245
+ dof = n_pts - n_p
246
+ chi_sq_red = ss_res / max(dof, 1)
247
+ rmse = float(np.sqrt(ss_res / n_pts))
248
+
249
+ errors = _param_errors(cost, p_free_opt, lb, ub, dof, chi_sq_red, n_p)
250
+
251
+ return {
252
+ "params": p_opt,
253
+ "param_names": list(mdl.param_names),
254
+ "errors": errors,
255
+ "residuals": residuals,
256
+ "z_fit": z_fit,
257
+ "r2": r2,
258
+ "rmse": rmse,
259
+ "chi_sq_red": chi_sq_red,
260
+ "model_name": mdl.name,
261
+ "n_points": int(n_pts),
262
+ "n_free": int(n_p),
263
+ "exit_flag": 1 if res.success else 0,
264
+ }
265
+
266
+
267
+ def _param_errors(
268
+ cost: Any,
269
+ p_free_opt: NDArray[np.float64],
270
+ lb: NDArray[np.float64],
271
+ ub: NDArray[np.float64],
272
+ dof: int,
273
+ chi_sq_red: float,
274
+ n_p: int,
275
+ ) -> NDArray[np.float64]:
276
+ """1-sigma errors via the numerical Hessian, mapped through the transform Jacobian."""
277
+ errors = np.full(n_p, np.nan, dtype=float)
278
+ if dof <= 0:
279
+ return errors
280
+ hess = _numerical_hessian(cost, p_free_opt)
281
+ try:
282
+ cov_free = np.linalg.inv(hess / 2) * chi_sq_red
283
+ except np.linalg.LinAlgError:
284
+ return errors
285
+ diag = np.diag(cov_free)
286
+ if np.all(diag >= 0):
287
+ se_free = np.sqrt(diag)
288
+ for k in range(n_p):
289
+ errors[k] = se_free[k] * abs(_bound_jacobian(p_free_opt[k], lb[k], ub[k]))
290
+ return errors
@@ -0,0 +1,156 @@
1
+ """2D surface model library. Port of MATLAB ``fitting.surfaceModels``.
2
+
3
+ A registry of named ``z = f(p, x, y)`` surface models (the 2D analogue of
4
+ ``calc/peakshapes``) used by 2D surface fitting and RSM peak extraction. Each
5
+ model carries its parameter names and a human-readable equation. Widths/sigmas
6
+ are floored at machine epsilon (matching MATLAB's ``max(p, eps)``) so a
7
+ degenerate fit never divides by zero.
8
+
9
+ Pure calc layer — ndarray in -> ndarray out; no fastapi/pydantic.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+ from collections.abc import Callable, Sequence
16
+ from dataclasses import dataclass
17
+
18
+ import numpy as np
19
+ from numpy.typing import ArrayLike, NDArray
20
+
21
+ __all__ = ["SurfaceModel", "surface_models", "get_surface_model"]
22
+
23
+ _EPS = sys.float_info.epsilon
24
+
25
+ _Arr = NDArray[np.float64]
26
+ # Parameter vector: a plain sequence or a numpy array (both index the same way).
27
+ PVec = Sequence[float] | _Arr
28
+ SurfaceFunc = Callable[[PVec, _Arr, _Arr], _Arr]
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class SurfaceModel:
33
+ name: str
34
+ func: SurfaceFunc
35
+ param_names: tuple[str, ...]
36
+ description: str
37
+
38
+ @property
39
+ def n_params(self) -> int:
40
+ return len(self.param_names)
41
+
42
+
43
+ def _xy(x: ArrayLike, y: ArrayLike) -> tuple[_Arr, _Arr]:
44
+ return np.asarray(x, dtype=float), np.asarray(y, dtype=float)
45
+
46
+
47
+ def _plane(p: PVec, x: _Arr, y: _Arr) -> _Arr:
48
+ return np.asarray(p[0] * x + p[1] * y + p[2], dtype=float)
49
+
50
+
51
+ def _paraboloid(p: PVec, x: _Arr, y: _Arr) -> _Arr:
52
+ return np.asarray(
53
+ p[0] * x**2 + p[1] * y**2 + p[2] * x * y + p[3] * x + p[4] * y + p[5], dtype=float
54
+ )
55
+
56
+
57
+ def _gaussian2d(p: PVec, x: _Arr, y: _Arr) -> _Arr:
58
+ sx = max(p[2], _EPS)
59
+ sy = max(p[4], _EPS)
60
+ z = p[0] * np.exp(-((x - p[1]) ** 2 / (2 * sx**2) + (y - p[3]) ** 2 / (2 * sy**2))) + p[5]
61
+ return np.asarray(z, dtype=float)
62
+
63
+
64
+ def _lorentzian2d(p: PVec, x: _Arr, y: _Arr) -> _Arr:
65
+ wx = max(p[2], _EPS)
66
+ wy = max(p[4], _EPS)
67
+ z = p[0] / (1 + ((x - p[1]) / wx) ** 2 + ((y - p[3]) / wy) ** 2) + p[5]
68
+ return np.asarray(z, dtype=float)
69
+
70
+
71
+ def _pseudo_voigt2d(p: PVec, x: _Arr, y: _Arr) -> _Arr:
72
+ amp, x0, y0, z0 = p[0], p[1], p[3], p[5]
73
+ wx = max(p[2], _EPS)
74
+ wy = max(p[4], _EPS)
75
+ eta = max(min(p[6], 1.0), 0.0) # clamp to [0, 1]
76
+ gauss = amp * np.exp(-((x - x0) ** 2 / (2 * wx**2) + (y - y0) ** 2 / (2 * wy**2)))
77
+ loren = amp / (1 + ((x - x0) / wx) ** 2 + ((y - y0) / wy) ** 2)
78
+ return np.asarray(eta * loren + (1 - eta) * gauss + z0, dtype=float)
79
+
80
+
81
+ def _poly2d(p: PVec, x: _Arr, y: _Arr) -> _Arr:
82
+ return np.asarray(
83
+ p[0] + p[1] * x + p[2] * y + p[3] * x**2 + p[4] * x * y + p[5] * y**2, dtype=float
84
+ )
85
+
86
+
87
+ def _exp_decay2d(p: PVec, x: _Arr, y: _Arr) -> _Arr:
88
+ tx = max(p[1], _EPS)
89
+ ty = max(p[2], _EPS)
90
+ return np.asarray(p[0] * np.exp(-x / tx - y / ty) + p[3], dtype=float)
91
+
92
+
93
+ # Wrap each kernel so callers may pass any array-like x/y (matches MATLAB's
94
+ # element-wise handles) and always receive float64 ndarrays.
95
+ def _wrap(fn: SurfaceFunc) -> SurfaceFunc:
96
+ def wrapped(p: PVec, x: ArrayLike, y: ArrayLike) -> NDArray[np.float64]:
97
+ xa, ya = _xy(x, y)
98
+ return fn(p, xa, ya)
99
+
100
+ return wrapped
101
+
102
+
103
+ _CATALOG: tuple[SurfaceModel, ...] = (
104
+ SurfaceModel("Plane", _wrap(_plane), ("a", "b", "c"), "z = a·x + b·y + c"),
105
+ SurfaceModel(
106
+ "Paraboloid",
107
+ _wrap(_paraboloid),
108
+ ("a", "b", "c", "d", "e", "f"),
109
+ "z = a·x² + b·y² + c·xy + d·x + e·y + f",
110
+ ),
111
+ SurfaceModel(
112
+ "2D Gaussian",
113
+ _wrap(_gaussian2d),
114
+ ("A", "x0", "sx", "y0", "sy", "z0"),
115
+ "z = A·exp(-((x-x0)²/(2σx²) + (y-y0)²/(2σy²))) + z0",
116
+ ),
117
+ SurfaceModel(
118
+ "2D Lorentzian",
119
+ _wrap(_lorentzian2d),
120
+ ("A", "x0", "wx", "y0", "wy", "z0"),
121
+ "z = A / (1 + ((x-x0)/wx)² + ((y-y0)/wy)²) + z0",
122
+ ),
123
+ SurfaceModel(
124
+ "2D Pseudo-Voigt",
125
+ _wrap(_pseudo_voigt2d),
126
+ ("A", "x0", "wx", "y0", "wy", "z0", "eta"),
127
+ "z = η·Lorentzian + (1-η)·Gaussian + z0 (0 ≤ η ≤ 1)",
128
+ ),
129
+ SurfaceModel(
130
+ "Polynomial 2D",
131
+ _wrap(_poly2d),
132
+ ("a00", "a10", "a01", "a20", "a11", "a02"),
133
+ "z = a00 + a10·x + a01·y + a20·x² + a11·xy + a02·y²",
134
+ ),
135
+ SurfaceModel(
136
+ "Exponential Decay 2D",
137
+ _wrap(_exp_decay2d),
138
+ ("A", "tx", "ty", "z0"),
139
+ "z = A·exp(-x/τx - y/τy) + z0",
140
+ ),
141
+ )
142
+
143
+ _BY_NAME = {m.name: m for m in _CATALOG}
144
+
145
+
146
+ def surface_models() -> tuple[SurfaceModel, ...]:
147
+ """The built-in 2D surface model catalog (in display order)."""
148
+ return _CATALOG
149
+
150
+
151
+ def get_surface_model(name: str) -> SurfaceModel:
152
+ """Look up a model by display name (e.g. ``"2D Gaussian"``)."""
153
+ try:
154
+ return _BY_NAME[name]
155
+ except KeyError:
156
+ raise ValueError(f"unknown surface model {name!r}") from None
@@ -0,0 +1,119 @@
1
+ r"""Thermal-property calculators (DiraCulator ``buildThermalTab``).
2
+
3
+ Pure calc layer — closed-form scalars in, result dicts out. No fastapi /
4
+ pydantic imports. Ports the three MATLAB ``buildThermalTab`` cards verbatim:
5
+
6
+ .. math::
7
+
8
+ \kappa = L_0\,\sigma\,T \qquad
9
+ \Theta_D = \frac{\hbar}{k_B}\,v_s\,(6\pi^2 n)^{1/3} \qquad
10
+ \alpha = \frac{\kappa}{\rho\,c_p}
11
+
12
+ Units follow the MATLAB toolbox (the behavioural reference): the
13
+ Wiedemann-Franz card takes electrical conductivity σ in S/cm (converted to
14
+ S/m internally) and temperature T in K, returning thermal conductivity κ in
15
+ W/(m·K). The Debye card takes the average sound velocity v_s in m/s and the
16
+ atomic number density n in atoms/m³, returning Θ_D in K. The diffusivity card
17
+ takes κ in W/(m·K), mass density ρ in kg/m³ and specific heat c_p in J/(kg·K),
18
+ returning α in m²/s (and mm²/s).
19
+
20
+ Reference values (closed-form physics, no MATLAB freeze available):
21
+ - ``wiedemann_franz(6e5, 300) -> kappa = 439.2`` W/(m·K) (Cu-like)
22
+ - ``debye_temperature(5000, 5e28) -> theta_D ≈ 548`` K
23
+ - ``thermal_diffusivity(150, 2329, 700) -> alpha ≈ 9.2e-5`` m²/s (Si)
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any
29
+
30
+ from quantized.calc.constants import constants
31
+
32
+ __all__ = [
33
+ "debye_temperature",
34
+ "thermal_diffusivity",
35
+ "wiedemann_franz",
36
+ ]
37
+
38
+ # Lorenz number (Sommerfeld value): L0 = pi^2 kB^2 / (3 e^2) = 2.44e-8 W*Ohm/K^2.
39
+ # Calibrated constant copied verbatim from the MATLAB source (do not "fix").
40
+ _LORENZ = 2.44e-8 # W*Ohm/K^2
41
+
42
+ _S_PER_CM_TO_S_PER_M = 100.0 # 1 S/cm = 100 S/m
43
+
44
+
45
+ def wiedemann_franz(sigma: float, temperature: float) -> dict[str, float]:
46
+ """Electronic thermal conductivity κ = L₀·σ·T (W/(m·K)).
47
+
48
+ Wiedemann-Franz law linking electrical and thermal conductivity. The
49
+ electrical conductivity σ is given in S/cm (Cu ≈ 6e5, Au ≈ 4.5e5,
50
+ Al ≈ 3.8e5) and converted to S/m internally; T is in K.
51
+
52
+ Args:
53
+ sigma: electrical conductivity (S/cm), ≥ 0.
54
+ temperature: temperature (K), > 0.
55
+
56
+ >>> round(wiedemann_franz(6e5, 300.0)["kappa"], 1)
57
+ 439.2
58
+ """
59
+ if sigma < 0:
60
+ raise ValueError("conductivity sigma must be non-negative")
61
+ if temperature <= 0:
62
+ raise ValueError("temperature must be positive")
63
+ sigma_si = sigma * _S_PER_CM_TO_S_PER_M # S/cm -> S/m
64
+ kappa = _LORENZ * sigma_si * temperature # W/(m*K)
65
+ return {
66
+ "kappa": kappa,
67
+ "sigma": sigma,
68
+ "temperature": temperature,
69
+ "lorenz": _LORENZ,
70
+ }
71
+
72
+
73
+ def debye_temperature(v_s: float, n: float) -> dict[str, float]:
74
+ """Debye temperature Θ_D = (ħ/k_B)·v_s·(6π²·n)^(1/3) (K).
75
+
76
+ From an average sound velocity and the atomic number density.
77
+
78
+ Args:
79
+ v_s: average sound velocity (m/s), > 0.
80
+ n: atomic number density (atoms/m³), > 0.
81
+
82
+ >>> round(debye_temperature(5000.0, 5e28)["theta_D"])
83
+ 548
84
+ """
85
+ if v_s <= 0:
86
+ raise ValueError("sound velocity v_s must be positive")
87
+ if n <= 0:
88
+ raise ValueError("atom number density n must be positive")
89
+ consts = constants()
90
+ hbar = consts["hbar"]
91
+ k_b = consts["kB"]
92
+ theta_d = (hbar / k_b) * v_s * (6.0 * 3.141592653589793**2 * n) ** (1.0 / 3.0)
93
+ return {"theta_D": theta_d, "v_s": v_s, "n": n}
94
+
95
+
96
+ def thermal_diffusivity(kappa: float, rho: float, cp: float) -> dict[str, Any]:
97
+ """Thermal diffusivity α = κ/(ρ·c_p) (m²/s).
98
+
99
+ Governs transient heat conduction — smaller α means slower thermal
100
+ equilibration. Also reports α in mm²/s (α·1e6).
101
+
102
+ Args:
103
+ kappa: thermal conductivity (W/(m·K)), > 0.
104
+ rho: mass density (kg/m³), > 0.
105
+ cp: specific heat (J/(kg·K)), > 0.
106
+
107
+ >>> round(thermal_diffusivity(150.0, 2329.0, 700.0)["alpha"], 7)
108
+ 9.2e-05
109
+ """
110
+ if kappa <= 0 or rho <= 0 or cp <= 0:
111
+ raise ValueError("kappa, rho and cp must be positive")
112
+ alpha = kappa / (rho * cp) # m^2/s
113
+ return {
114
+ "alpha": alpha,
115
+ "alpha_mm2": alpha * 1e6, # mm^2/s
116
+ "kappa": kappa,
117
+ "rho": rho,
118
+ "cp": cp,
119
+ }