quantized-lab 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,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
|
+
}
|